Concurrency with pthreads

Course 3 · Ch 3
Concurrency with pthreads
The same manual-discipline story as memory management — now applied to multiple threads at once

Every "manual control" story this course has told — pointers, memory, now data structures — has a concurrency counterpart. This chapter shows what happens when multiple threads touch the same data with nothing forcing them to coordinate.

Creating a Thread

void *worker(void *arg) { printf("Hello from a thread\n"); return NULL; } pthread_t t; pthread_create(&t, NULL, worker, NULL); // a function pointer + a void* argument pthread_join(t, NULL); // wait for it to finish

pthread_create's third argument is a function pointer — a real, direct application of c2-7's own material.

A Genuine Race Condition

int counter = 0; void *increment(void *arg) { for (int i = 0; i < 100000; i++) { counter++; // NOT atomic — read, modify, write, in three separate steps } return NULL; }

Two threads both running this concurrently will, almost every time, produce a final counter value less than 200,000 — counter++ is really read-then-modify-then-write, and two threads can interleave those three steps, silently losing updates. The exact final value varies from run to run — a genuinely non-deterministic bug.

Mutexes

pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; void *increment(void *arg) { for (int i = 0; i < 100000; i++) { pthread_mutex_lock(&lock); counter++; pthread_mutex_unlock(&lock); } return NULL; }

Locking around the critical section ensures only one thread executes it at a time — the race is genuinely fixed.

What C Does Not Check

This is the real payoff. Nothing in C's type system or compiler prevents forgetting to lock lock before touching counter — any thread can read or write counter directly, at any point, with zero compile-time enforcement. The mutex is purely a convention the programmer must remember to follow at every single access site, with nothing tying the mutex to the data it's meant to protect.

Rust's Send and Sync — Fearless Concurrency, Revisited

rust2-5's own "fearless concurrency" is exactly the fix for what C leaves entirely unchecked. Send and Sync are marker traits the compiler checks — a type that isn't Sync genuinely cannot be shared across threads without going through a synchronization primitive like Mutex<T> at all; the code simply won't compile otherwise. Critically, Rust's Mutex<T> wraps the data itself — the compiler refuses to compile any access to the inner value without first acquiring the lock. In C, the mutex and the data are two separate, unrelated variables, connected by nothing but the programmer's own memory.

ConceptRustC
Mutex-to-data relationshipMutex<T> wraps the data — enforced by the type systemtwo separate variables — connected only by convention
Accessing data without lockingcompile error — the data isn't reachable at allcompiles fine — a genuine, undetected race condition
Unlocking on an early returnautomatic — MutexGuard's Drop unlocks itmanual — every exit path must call unlock explicitly
Name mutexes after what they protect, and document it
Since nothing in the language ties a mutex to its data, clear naming and comments are the only thing standing in for the compile-time guarantee Rust provides automatically.
A forgotten unlock deadlocks the next lock attempt
An early return or an error path that skips pthread_mutex_unlock leaves the mutex permanently locked — the next thread to call lock on it blocks forever. Rust's MutexGuard unlocks automatically via Drop when it goes out of scope, on every exit path, including early returns — the same automatic-cleanup guarantee c2-2 already established for memory, now applying to locks as well.

Coding Challenges

Challenge 1

Write a program that spawns two threads, each incrementing a shared global counter 100,000 times with no mutex protection. Run it a few times and report whether the final value is consistently 200,000.

📄 View solution
Challenge 2

Fix Challenge 1's program by adding a pthread_mutex_t around the increment. Run it several times and confirm the final value is now consistently and correctly 200,000.

📄 View solution
Challenge 3

Explain precisely why Rust's Mutex<T> makes an unprotected access to shared data a compile error, while C's pthread_mutex_t makes the identical mistake something that compiles and runs, with only sometimes-visible consequences.

📄 View solution

Chapter 3 Quick Reference

  • pthread_create/pthread_join — spawn and wait for a thread; the entry point is a function pointer
  • counter++ is read-modify-write, not atomic — concurrent access without synchronization is a real race condition
  • pthread_mutex_lock/unlock around a critical section fixes the race
  • Nothing in C ties a mutex to the data it protects — pure programmer convention, unchecked by the compiler
  • Rust's Mutex<T> wraps the data itself, and MutexGuard's automatic unlock (via Drop) prevents forgotten-unlock deadlocks entirely
  • Next chapter: Undefined Behavior Deep Dive — every UB thread this course has flagged, finally covered in full