In our previous lesson, we examined how operating systems manage memory when demand exceeds supply, comparing page replacement algorithms like FIFO and LRU. That discussion centered on the OS managing resources for what can be thought of as independent programs. Now, we turn our attention inward, to the complexities that arise within a single program when multiple threads of execution operate concurrently.
This lesson tackles one of the most fundamental challenges in concurrent programming: the race condition. You will learn what causes a race condition, how to spot the vulnerability in a code snippet, and how to use a mutex—a foundational synchronization primitive—to protect shared data and ensure your program behaves correctly. This is a critical skill for building reliable and robust multi-threaded applications.
The Problem of Shared State
As we've previously discussed, threads within the same process share the same memory space, including global variables and the heap. This is powerful, as it allows threads to collaborate by working on the same data. However, it also introduces a significant risk.
Race Conditions, Synchronization, Mutexes
This document from Brown University's CS course provides a clear explanation of the problem. It begins by recapping what resources threads share and then dives into a practical code example.
First, review the table comparing resource sharing between processes and threads in the introduction. This reinforces why this problem exists for threads. Next, read the section on Race Conditions. The C++ code snippet (incr-basic.cc) demonstrates a simple multi-threaded program that increments a shared counter. The key insight is in the explanation of why the final result is incorrect. Pay close attention to how a single C++ operation like *x += 1; is broken down into multiple, non-atomic hardware instructions: load, add, and store. The non-deterministic interleaving of these micro-operations across different threads is the root cause of the race condition.
The scenario described in the reading—where the final value of the counter depends on the unpredictable timing of thread execution—is the essence of a race condition. The result of the computation "races" to a different outcome depending on which thread gets there first. This non-determinism can lead to insidious bugs that are difficult to reproduce and debug.
At its core, a race condition happens because an operation that you might assume is a single, indivisible step is actually a sequence of smaller steps. If the thread scheduler pauses one thread in the middle of this sequence and lets another thread run, the second thread may operate on stale data, leading to lost updates.
Identifying Race Conditions
The fundamental rule for spotting a potential race condition is straightforward: if two or more threads can concurrently access a shared piece of data, and at least one of those accesses is a write, you must have synchronization.
Let's look at a classic code example.
Mutexes and Race Conditions: Mutual exclusion locks ( ...
This document from IIITA provides a C code example using the POSIX threads (pthreads) library, which is a common C interface for multi-threading.
Read the section Exercise 1. This describes a program, badcnt.c, that demonstrates the race condition. Then, read the "Reason" section that follows (The reason). It provides a step-by-step table showing a "bad" interleaving where an increment is lost, which should solidify your understanding of the load-modify-store problem.
Now, consider the following scenario. Does this code have a race condition?
Yes, it absolutely has a race condition. The mutex in Thread1 only prevents other threads that also try to acquire the same mutex from running concurrently. Since Thread2 does not use the mutex, it can access and modify x at any time, including while Thread1 is in the middle of its load-modify-store sequence. For synchronization to work, all threads that access the shared resource must agree to follow the same protocol.
The Solution: Mutual Exclusion and Mutexes
To prevent race conditions, we must enforce mutual exclusion. This principle states that if one thread is executing a sensitive piece of code that manipulates shared data, no other thread may enter that same region of code. This protected region is known as a critical section.
The most common mechanism for enforcing mutual exclusion is a mutex (short for mutual exclusion). A mutex is essentially a lock. Before entering a critical section, a thread must acquire the lock. If the lock is already held by another thread, the current thread will be forced to wait (it blocks) until the lock is released. Once the thread has finished its work in the critical section, it releases the lock, allowing another waiting thread to acquire it and proceed.

Let's see how to apply this in practice to fix the buggy counter program. The following video provides a clear, hands-on demonstration using the pthreads library in C.
What is a mutex in C? (pthread_mutex)
This tutorial from the CodeVault channel walks through the process of identifying a race condition and then fixing it using a pthread_mutex.
Watch the initial demonstration of the problem from the beginning, where running the multi-threaded program produces an incorrect result. Next, see how a pthread_mutex_t variable is declared and initialized using pthread_mutex_init and pthread_mutex_destroy in the segment from initializing the mutex. Finally, watch the core of the solution from locking and unlocking. This shows how to wrap the critical section (mails++) between pthread_mutex_lock() and pthread_mutex_unlock() to guarantee the correct result.
The general pattern for using a mutex is as follows:
- Initialize the mutex once before any threads begin to use it.
pthread_mutex_init(&my_mutex, NULL);
- Lock the mutex just before entering the critical section.
pthread_mutex_lock(&my_mutex);
- Perform the operations on the shared data within the critical section.
- Unlock the mutex immediately after leaving the critical section.
pthread_mutex_unlock(&my_mutex);
- Destroy the mutex when it is no longer needed (e.g., after all threads have completed).
pthread_mutex_destroy(&my_mutex);
This lock-unlock pairing ensures that the operations within the critical section are executed atomically from the perspective of other threads. While one thread holds the lock, no other thread can acquire it, effectively serializing access to the shared data and eliminating the race condition.
How Does a Mutex Itself Work?
A natural follow-up question is: how does the mutex itself avoid a race condition? If pthread_mutex_lock() is just checking and setting a value, couldn't two threads race to acquire the lock itself?
This is a valid concern, and the solution lies in the hardware. Modern processors provide special atomic instructions—operations that are guaranteed by the hardware to be uninterruptible.
how does a Mutex even work? (atoms in the computer??)
This video from the Low Level channel gives a great high-level explanation of the mechanism behind mutexes.
Watch from how a mutex works. The key takeaway is that the check-and-set operation for the lock's state is not a normal sequence of instructions. It's performed using a special, uninterruptible atomic instruction (like "compare-and-swap") provided by the CPU. This hardware guarantee is the bedrock upon which software synchronization primitives like mutexes are built.
Conclusion
In this lesson, you've learned to identify one of the most common pitfalls of concurrent programming and how to resolve it.
Key Takeaways:
- A race condition occurs when multiple threads access shared data concurrently, with at least one write, and the program's outcome depends on the unpredictable order of execution.
- This is typically caused by operations that seem atomic (e.g.,
x++) but are actually composed of multiple, interruptible instructions (load, modify, store). - The code that accesses the shared data is called the critical section.
- A mutex is a synchronization primitive used to enforce mutual exclusion, ensuring that only one thread can be inside a critical section at any given time.
- To use a mutex, you
lock()it before the critical section andunlock()it after. This protocol must be followed by all threads accessing the shared data. - Mutexes themselves rely on hardware-level atomic operations to function correctly without introducing their own race conditions.
You now understand how to protect a simple shared variable. However, threads often need to coordinate in more complex ways. For instance, what if one thread is producing data and another is consuming it? A simple lock isn't enough; the consumer thread might need to wait until there is data available. Our next lesson will introduce semaphores, a more versatile synchronization tool designed to handle exactly these kinds of producer-consumer problems.