Skip to main content
C Thread Synchronization

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”. Unlike fork(), 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:
What actually happens (assembly level):
  1. Thread A reads counter (value: 100)
  2. Thread B reads counter (value: 100)
  3. Thread A increments to 101, writes back
  4. Thread B increments to 101, writes back
  5. Final value: 101 (should be 102!)
This is called a lost update - one thread’s work was silently discarded.

The Solution: Synchronization Primitives

Thread Synchronization Use synchronization primitives (mutexes, condition variables, atomics) to ensure only one thread accesses shared data at a time. The diagram above shows a classic Producer-Consumer pattern where:
  • 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
Key Insight: Synchronization trades performance (threads must wait) for correctness (data remains consistent).

Mutexes

Mutex Types


Condition Variables

Always use a while loop with condition variables, not if! Spurious wakeups can occur, where the thread wakes up even though no signal was sent.

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

Deadlock Visualization Deadlocks occur when threads wait for each other in a circular dependency. The diagram above shows the classic scenario where Thread 1 holds Mutex A and wants Mutex B, while Thread 2 holds Mutex B and wants Mutex A.

Four Conditions for Deadlock (Coffman Conditions)

A deadlock can only occur if ALL four conditions are present:
  1. Mutual Exclusion: Resources cannot be shared (mutexes by definition)
  2. Hold and Wait: Thread holds resources while waiting for more
  3. No Preemption: Resources cannot be forcibly taken from threads
  4. Circular Wait: Circular chain of threads waiting for resources
Prevention Strategy: Break at least one of these conditions.

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 Use pthread_mutex_timedlock() and retry:
3. Try-Lock and Backoff
Best Practice: Design your system to minimize the number of locks needed simultaneously. Consider using lock-free data structures or message passing instead of shared memory.

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