TĐ

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 family
    • kill -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_routineentry 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
    1. Both threads read x before either writes.
    2. Both compute temp value x+1.
    3. 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)
    1. Consumer locks ➜ data missing ➜ pthread_cond_wait ➜ mutex released → sleep.
    2. Producer locks, writes data, pthread_cond_signal, unlocks.
    3. 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.