Concurrency & Threading
Modern systems demand concurrent programming. Let’s master POSIX threads and synchronization primitives. A useful mental model: threads are like multiple cooks in a shared kitchen. They can all work simultaneously (parallelism), but if two cooks try to use the same knife at the same time, someone gets cut (data corruption). Synchronization primitives are the rules that prevent collisions — “only one person uses the stove at a time” (mutex), “wait until the ingredients are prepped before cooking” (condition variable), “anyone can read the recipe, but only one person can write changes” (read-write lock).POSIX Threads Basics
Threads are “lightweight processes”. Unlikefork(), which creates a copy of the process, threads share the same memory space (heap, data segments, file descriptors) but have their own stack and registers.
Key differences:
- Shared Memory: All threads can access global variables and the heap.
- Independent Execution: Each thread runs independently (scheduled by the kernel).
- Low Overhead: Creating a thread is much faster than creating a process.
Thread Attributes
Why Synchronization Matters
When multiple threads access shared data concurrently, race conditions occur. Without proper synchronization, the final state of your data becomes unpredictable.The Problem: Race Conditions
Consider two threads incrementing a counter:- Thread A reads counter (value: 100)
- Thread B reads counter (value: 100)
- Thread A increments to 101, writes back
- Thread B increments to 101, writes back
- Final value: 101 (should be 102!)
The Solution: Synchronization Primitives
- Mutex provides mutual exclusion (only one thread in critical section)
- Condition variables allow threads to wait for specific conditions
- Bounded queue is the shared resource protected by synchronization
Mutexes
Mutex Types
Condition Variables
Read-Write Locks
Thread-Local Storage
Thread Pool
Atomics (C11)
Atomics provide lock-free thread safety for individual variables. A common misconception:volatile does NOT provide thread safety in C. volatile only prevents the compiler from caching the value in a register — it says nothing about CPU caches, memory ordering, or atomicity. For thread-safe shared variables, you need _Atomic (or explicit mutexes). This is a frequent source of subtle bugs in code ported from single-core embedded systems to multi-core servers.
Common Pitfalls
Deadlock
Preventing Deadlocks
Four Conditions for Deadlock (Coffman Conditions)
A deadlock can only occur if ALL four conditions are present:- Mutual Exclusion: Resources cannot be shared (mutexes by definition)
- Hold and Wait: Thread holds resources while waiting for more
- No Preemption: Resources cannot be forcibly taken from threads
- Circular Wait: Circular chain of threads waiting for resources
Deadlock Prevention Techniques
1. Lock Ordering (Most Common) Always acquire locks in the same global order across all threads - this breaks the circular wait condition. 2. Lock Timeout Usepthread_mutex_timedlock() and retry:
Race Condition (TOCTOU — Time of Check to Time of Use)
The check-then-act pattern is one of the most common concurrency bugs. Between the moment you check a condition and the moment you act on it, another thread can change the state, making your action invalid. This class of bug is called TOCTOU (Time Of Check to Time Of Use) and it appears not just in threading but also in file system operations (checking if a file exists, then opening it — another process can delete it in between).Exercises
1
Dining Philosophers
Implement the dining philosophers problem with proper deadlock prevention.
2
Reader-Writer Problem
Implement a solution that doesn’t starve either readers or writers.
3
Barrier
Implement a reusable barrier that allows N threads to synchronize.
4
Thread-Safe Queue
Implement a lock-free MPSC (multi-producer, single-consumer) queue.
Next Up
Network Programming
Build network applications with sockets