Threads, Processes, and Synchronization – Detailed Study Notes
Process Management Recap (ps & kill)
ps -a
- Lists all processes currently running.
- Information shown: PID, user, start time, command name.
kill
familykill -KILL <pid>
(a.k.a. SIGKILL
)- Uninterruptible; OS immediately removes the process from the scheduler.
kill -TERM <pid>
(SIGTERM
)- Politely asks the process to perform graceful shutdown (similar to clicking the ❌ on a GUI app).
kill -STOP <pid>
/ kill -CONT <pid>
- Pause (
STOP
) or resume (CONT
) a process without terminating it.
Job Control in the Shell
- Background execution
- Appending
&
after a command (e.g. xeyes &
) starts the job detached from the terminal.
Ctrl-Z
- Sends
SIGSTOP
; pauses the foreground job and returns prompt control.
jobs
- Lists all jobs launched from the current shell, gives numeric job IDs.
bg [n]
- Resume job n in the background.
fg [n]
- Bring job n to the foreground; terminal now interacts directly with that process.
- Typical workflow
- Run two
xeyes &
➜ both in background. jobs
➜ see [1]
and [2]
.fg 1
➜ first xeyes
becomes foreground, terminal now occupied until Ctrl-Z
.
Threads vs. Processes
- Shared vs. isolated memory
- Processes: separate address spaces.
- Threads: share the same code, data, and heap segments.
- Same PID, different execution contexts
- OS schedules threads as part of one process.
- Hardware with simultaneous multi-threading may execute them in parallel, yet the kernel still treats them as a single PID.
- Motivation example — Video game
- Thread A: game logic (physics, A.I.).
- Thread B: rendering.
- Separation prevents a GPU-slowdown from stalling physics; avoids heavy IPC that two processes would need.
Thread Memory Model
- Each thread has its own stack
- Distinct call frames, local variables.
- Common heap / global data ➜ enables direct pointer sharing.
Creating Threads in C (POSIX pthread API)
- Header:
#include <pthread.h>
- Compile:
gcc -pthread -o my_exe file1.c file2.c
-pthread
flag instructs the compiler & linker to link thread-safe libc, adjust memory model.
- Prototype
pthread_create(pthread_t *tid, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg);
tid
— storage for new thread ID.attr
— pointer to attribute object (can be NULL
).start_routine
— entry point of the thread.arg
— single void *
parameter passed to the routine.
Requirements for a Thread Start Function
- Must have signature:
void *func(void *arg)
- Returns
void *
(exit status or pointer to data). - Accepts exactly one pointer argument (can be struct-cast to anything).
Shared-Memory Pitfalls — Race Conditions
- Simple example
- Global
int x = 0;
- Function:
void incrementX(){ x = x + 1; }
(x = x + 1) - Thread A calls it 100 times, Thread B 50 times.
- Expected final value: 150. Reality may be < 150 due to interleaving of read–modify–write steps.
- Mini-timeline
- Both threads read
x
before either writes. - Both compute temp value
x+1
. - Last write overwrites previous increment.
- Definition: Race Condition
- Output depends on relative timing of concurrent events that access shared state.
- Real-world metaphor: you attempt to pick up a cup before I finish putting it down.
Mutexes (Mutual Exclusion)
- Declaring:
pthread_mutex_t my_mutex;
- Operations
pthread_mutex_lock(&my_mutex);
— blocks until lock acquired.pthread_mutex_unlock(&my_mutex);
— releases.
- Conch-shell analogy ("Lord of the Flies")
- One entity owns the mutex ➜ gains exclusive right to touch shared memory.
- Cost model
- Locking can block; unlocking is cheap.
- Too fine-grained (one mutex per variable) ➜ overhead; too coarse-grained ➜ contention.
Producer–Consumer Pattern
- Motivation
- Production and consumption rates differ (e.g., boil egg 5\,\text{min} vs. eat in seconds).
- Multiple producers / consumers can smooth throughput.
- Analogies
- Mailbox: many postmen, one reader ➜ want a signal rather than constant polling.
- Dining Hall: one cook, many diners; cook rings bell when food ready.
Condition Variables (pthread_cond_t
)
- Declaring a condition:
pthread_cond_t cond;
- Waiting (consumer)
- Must hold associated mutex.
- Call
pthread_cond_wait(&cond, &mutex);
- Atomically: releases mutex, sleeps until signaled, then re-locks before return.
- Signaling (producer)
- After producing and holding mutex:
pthread_cond_signal(&cond);
- Optionally
pthread_cond_broadcast
to wake all waiters.
- Typical timeline (excerpt)
- Consumer locks ➜ data missing ➜
pthread_cond_wait
➜ mutex released → sleep. - Producer locks, writes data,
pthread_cond_signal
, unlocks. - Consumer wakes, reacquires mutex, processes data.
Thread Termination & Coordination
- Natural return from start function ➜ thread ends, last stack frame removed.
- Explicit
pthread_exit(void *retval)
- Allows returning status/data pointer.
- Forceful
pthread_cancel(pthread_t tid)
- External thread requests termination (e.g., stop consumers once production done).
- Process exit (e.g.,
exit()
or exec*
) kills all threads. pthread_join(tid, void **ret);
- Calling thread (often
main
) blocks until tid
terminates. - Retrieves
retval
from pthread_exit
or function return. - Good practice: join every joinable thread to reclaim resources.
Thread Attributes (Optional attr
argument)
- Use
pthread_attr_t attr; pthread_attr_init(&attr);
- Common adjustments
- Detached state:
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
- Detached threads free their resources automatically; cannot be joined.
- Scheduling policy (FIFO, Round-Robin), stack size, CPU affinity, etc.
- Always
pthread_attr_destroy(&attr);
when done.
Best-Practice Advice & Warnings
- Use threads sparingly; concurrency multiplies complexity.
- Always design and reason on paper first; race conditions often disappear when you add
printf
or a debugger because timing changes. - Prefer coarse abstractions (e.g., producer–consumer queues) over ad-hoc shared variables.
- Practice drawing timelines of mutex/condition interactions to cement understanding.
- Remember: the hardest bugs are timing-dependent; a small code-change or log statement can mask them.