← Concurrency and Threads

Concurrency and Threads

Chapter notes

Thread creation: pthread_create

pthread_create

  • Spawns a new thread running fn(arg). The kernel decides when the child actually starts — could be before or after the parent’s next line.
  • Signature: int pthread_create(pthread_t *t, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg);
  • attr is almost always NULL (default attributes).

Arguments: one void * slot

  • Only a single void * gets through. To pass multiple values, bundle them in a struct and hand over its address:
typedef struct { int a; double b; } myarg_t;
myarg_t args = { 10, 12.34 };
pthread_create(&t, NULL, mythread, &args);
  • Inside the start routine, cast back: myarg_t *m = (myarg_t *) arg;.

Lifetime

sequenceDiagram
  participant P as Parent
  participant T as Child thread
  P->>T: pthread_create(fn, &args)
  T->>T: fn(args) runs
  T-->>P: returns void *retval
  P->>T: pthread_join(t, &retval)
  • The caller owns the storage you point arg at. If args lives on the parent’s stack and the parent returns before the child reads it, the child reads garbage.
  • A thread returning a pointer into its own stack is just as broken — the stack unwinds at exit. Use heap or a caller-owned buffer.

Error reporting

  • pthread_create returns the error code directly (0 on success).
  • Does not touch errno. Reading errno after pthread_create is a bug.
  • Common failure: EAGAIN (system out of resources for another thread).

Quiz yourself

  • When you need multiple arguments, what does the canonical pattern look like?
  • Why is returning a pointer to a stack variable from a thread dangerous?

Thread completion: pthread_join

pthread_join

  • Signature: int pthread_join(pthread_t t, void **retval);
  • Blocks the caller until thread t terminates.
  • *retval receives whatever the start routine returned (a void *).
  • Pass NULL as the second arg if you don’t care about the return value.

One joiner per thread

  • Each joinable thread can be joined exactly once. A second join is undefined behavior — typically returns garbage or crashes.
  • Two threads concurrently joining the same target is the same bug.
  • The OS won’t stop you — this is the kind of thing you need a habit to enforce, not a check at the API boundary.

Returning values out

void *worker(void *arg) {
  result_t *r = malloc(sizeof *r);
  r->status = OK;
  return r;                 // void * out
}

void *raw;
pthread_join(t, &raw);
result_t *r = raw;          // cast back
  • The thread returns a pointer; pthread_join writes that pointer into *retval.
  • NULL is just (void *)0 — perfectly legitimate, not “no return”.
  • Anything pointing at the joinee’s stack is invalid (see ch27-1).

Detach: skip the join

  • pthread_detach(t) tells the runtime “I won’t be joining this; clean it up yourself when it exits.”
  • Useful for fire-and-forget background work (logging, telemetry, request handlers that respond and forget).
  • Forgetting to join a joinable thread is a resource leak; detaching upfront eliminates that whole class of bug.

Quiz yourself

  • What goes wrong if two callers both try to pthread_join the same thread?
  • When should you prefer pthread_detach over pthread_join?

Locks & condition variables: the pthread API

Mutexes

sequenceDiagram
  participant T1 as Thread 1
  participant M as Mutex
  participant T2 as Thread 2
  T1->>M: pthread_mutex_lock()
  M-->>T1: acquired
  T2->>M: pthread_mutex_lock()
  Note right of M: T2 blocks
  T1->>M: pthread_mutex_unlock()
  M-->>T2: acquired
  • Lock before touching shared state, unlock right after.
  • pthread_mutex_lock blocks until the lock is yours; _trylock returns immediately with EBUSY if it’s held.
  • Forgetting to unlock is the classic deadlock cause.

Condition variables

  • A condition variable is a sleep-and-wake primitive: “wait until someone tells me something interesting happened.”
  • Always paired with a mutex. The protected predicate is what “something interesting” means — e.g., queue_nonempty.
pthread_mutex_lock(&m);
while (!ready) {            // loop, not if
  pthread_cond_wait(&cv, &m);
}
/* now safe to consume */
pthread_mutex_unlock(&m);
  • pthread_cond_wait(&cv, &m) atomically:
    1. Releases &m,
    2. Puts the thread to sleep on &cv,
    3. Re-acquires &m on wake-up before returning.

signal vs broadcast

  • pthread_cond_signal wakes one waiter — use when exactly one waiter can make progress per signal (typical producer/consumer).
  • pthread_cond_broadcast wakes all waiters — use when the predicate change might unblock multiple, or when you can’t reason about which one should wake.

Spurious wakeups

  • POSIX explicitly allows cond_wait to return without anyone signalling. The kernel and pthread implementation are free to wake waiters whenever.
  • Even without spurious wakeups, between wake and re-acquiring the mutex, another waiter could grab the thing you wanted.
  • Loop-check the predicate, every time. The wakeup is an invitation to re-check, not a guarantee.

Quiz yourself

  • Why does cond_wait require the mutex to be held?
  • Why must the predicate check be in a while loop, not an if?

Sign in to bring your own textbook — every completed chunk's notes stack here.