Mutexes and Locks
The mutex guards access. It is the mutual exclusion on the data, not a copy of the data and not a container for it.
When Several Threads Reach One Variable
Several threads running in one program can all reach the same variable. That is useful when the threads need to cooperate, but it creates a risk. Each thread may read the variable, change the value it read, and write the new value back. If two threads do these three steps at overlapping moments, the final value depends on the timing of the steps rather than on the logic the programmer wrote. The same code can produce different results on different runs.
A mutex is the tool that prevents this kind of overlap. This article follows a single small program in which one thread-safe update to a shared variable is guarded by a mutex. Before reading the explanation, pause and try to predict the trace question below. Your prediction does not need to be correct. It gives you something to check against the explanation that follows.
Two threads each add 1 to the same shared variable, which starts at 0. Before reading on, predict the possible final values. Is 2 guaranteed? Could it end as 1? Write down your reasoning, then keep reading to compare it with the explanation.
Hints
- Think about what happens if both threads read the variable before either one writes back.
- Ask yourself whether the program's logic alone determines the result, or whether timing also matters.
What a Mutex Guarantees
A mutex is a mutual exclusion object that controls access to shared data.
The phrase mutual exclusion describes the guarantee. Only one party can hold the mutex at a given moment. Any code protected by the mutex therefore runs with exclusive access to the data it manipulates. While one thread holds the mutex, no other thread can hold it at the same time.
The mutex does not hold the data itself. It is a controlling object that sits beside the data and mediates who may act on it. Think of the shared variable as a resource that every thread wants to change. Without a controlling object, each thread goes straight to the variable. With a mutex, each thread must pass through the mutex first, and only the thread that holds it may proceed to the data. The request goes to the mutex, and the data is reached only after the mutex grants the request. That ordering is the whole protection pattern.
The mutex guards access. It is the mutual exclusion on the data, not a copy of the data and not a container for it.
Acquiring the Mutex with a Lock
A thread gets hold of a mutex by performing a lock operation on it. The source describes this operation as a lock() call. In the code used in this lesson, the operation is expressed as the declaration of a lock object that refers to the mutex. Declaring the lock object is the acquisition step. Its scope then determines how long the mutex stays held.
1- m is the mutex that controls access to sh
- The declaration of lck acquires m at that exact line
- The update to sh runs under that hold
- The closing brace ends the scope of lck, which releases m
Tracing One Call to f
Follow one call to f from start to finish. The mutex begins unlocked. Reaching the lock declaration changes its status to held by this thread. The update to sh runs under that hold. When the function ends, the lock object goes out of scope and the mutex returns to unlocked. The diagram shows only the two states and the transitions between them.
Following one call to f
The mutex m starts unlocked and sh starts at 0. The function f is called once. What is the state of m at each step, and what is sh at the end?
- 1Before the call
m is unlocked and sh is 0.
- 2Lock declaration
The declaration of lck acquires m, so m becomes held by this thread.
- 3Update to sh
sh = sh + 1 runs while m is held, so sh becomes 1 with exclusive access.
- 4End of function
lck goes out of scope, so m returns to unlocked.
After the call, m is unlocked and sh is 1.
Release at the End of the Scope
The lock is released implicitly when the function's scope ends. The programmer does not write a separate unlock step in the lock declaration pattern shown here. The protected section is therefore the code that runs while the lock object is in scope. In the example above, the protected section is the single update to sh, because the lock object lives until the closing brace of f.
In a short function, a lock object is declared on the first line and the function has an early return on the third line. Before reading the rule, predict: at which point is the mutex released when that early return runs, and what determines it?
Hints
- The source ties release to the end of the lock object's scope.
- Ask what happens to the lock object when control leaves the function through the return.
The rule to remember is that the mutex stays held for as long as the lock object is in scope. Whatever ends that scope, whether it is the closing brace of the function or a return that leaves it early, ends the hold. The release is tied to scope, so the programmer does not have to remember a separate release call on each path out of the function. The source states this rule for the function's scope ending; use the same reasoning for any exit from that scope.
Two Threads Competing for One Mutex
Now consider two threads that both call f at about the same time. Because only one party can hold the mutex at a given moment, one thread acquires m first and proceeds to update sh. The other thread's lock request cannot be granted while the mutex is held, so it must wait. It proceeds only after the first thread releases the mutex, which happens when that thread's lock object goes out of scope.
Notice the order in the diagram. Thread A's update to sh happens only while Thread A holds the mutex. Thread B reaches the data only after the mutex has been granted to it. The two updates never overlap, which is the mutual exclusion the mutex was created to provide.
How Unprotected Updates Diverge
Without a mutex, nothing forces the two threads to take turns. Each thread can read the variable, change the value it read, and write it back at overlapping moments. The final value then depends on how the steps happened to interleave. The diagram below shows one interleaving that loses an update. Both threads read the same starting value, so each writes the same result, and one increment disappears.
int sh = 0;
void g() {
sh = sh + 1; // no lock: read, add, write can interleave
}Two threads can both read sh before either writes it back, so the final value depends on timing.
std::mutex m;
int sh = 0;
void g() {
std::unique_lock<std::mutex> lck {m};
sh = sh + 1; // read, add, write happen under the hold on m
}The update runs with exclusive access, so the two increments cannot overlap and both are applied.
Mistakes Beginners Make with Mutexes
Common mistake Touching the shared variable outside any lock declaration
What it looks likesh = sh + 1; placed before the line that declares the lock object in the same functionWhy it failsThe update runs before the mutex is acquired, so no exclusion applies to it.
Do this insteadPlace the lock declaration before the shared-data operation, so the update runs while the mutex is held.
Common mistake Using different mutex objects for the same shared data
What it looks likeOne thread locks a mutex m1 while another thread locks a separate mutex m2 and both update sh
Why it failsEach thread holds a different mutex, so they are not excluded from each other.
Do this insteadEvery thread that touches the shared data must use the same mutex object.
Common mistake Letting the lock's scope end before the protected work finishes
What it looks likeA lock object is declared inside a nested block that closes before the update to sh
Why it failsThe mutex is released as soon as the scope ends, so the update runs unprotected.
Do this insteadMake sure the lock's scope covers every line that needs protection.
Common mistake Assuming a mutex protects data it does not actually guard in the code
Checking a Program That Gives Wrong Answers
When a program gives a wrong answer with threads, the useful question is where the protected sequence broke. Check three things in order. A mismatch at any of these points produces the same symptom: the shared data changes without the exclusion you expected.
- 1Is the shared variable touched outside any lock declaration?
- 2Do all threads that touch the variable use the same mutex object?
- 3Does the lock's scope cover every line that needs protection?
Trace Check and Practice
A function declares a lock object on its first line, updates a shared variable on the second line, and then has a final line that does unrelated output. A second thread calls the same function. Explain which line runs while the mutex is held, when the first thread releases it, and why the second thread cannot update the variable at the same moment as the first.
Hints
- Identify the scope of the lock object first.
- Remember that release is tied to the lock object going out of scope, not to a specific line of output.
Key Takeaways
- A mutex is a mutual exclusion object that controls access to shared data, so only one party can hold it at a given moment.
- A thread acquires a mutex through a lock operation, and declaring the lock object performs that acquisition at the line where it appears.
- The lock is released implicitly when the scope of the lock object ends, so the protected section is the code that runs while the lock object is in scope.
- Every thread touching the shared data must use the same mutex, and the acquisition must come before the shared-data operation.
- Without the right mutex, read-modify-write steps can interleave and the final value depends on timing rather than on the program's logic.