Concurrency with pthreads
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
pthread_create's third argument is a function pointer — a real, direct application of c2-7's own material.
A Genuine Race Condition
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
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.
| Concept | Rust | C |
|---|---|---|
| Mutex-to-data relationship | Mutex<T> wraps the data — enforced by the type system | two separate variables — connected only by convention |
| Accessing data without locking | compile error — the data isn't reachable at all | compiles fine — a genuine, undetected race condition |
| Unlocking on an early return | automatic — MutexGuard's Drop unlocks it | manual — every exit path must call unlock explicitly |
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
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 solutionFix 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 solutionExplain 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 solutionChapter 3 Quick Reference
pthread_create/pthread_join— spawn and wait for a thread; the entry point is a function pointercounter++is read-modify-write, not atomic — concurrent access without synchronization is a real race conditionpthread_mutex_lock/unlockaround 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, andMutexGuard'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