Concepts / Mutexes and Locks

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.

  • Programming

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.

EASY

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

Definition

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.

Key point

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.

cpp
Output
1
Notes on the code
  • 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.

UnlockedHeld by this threadlock declaration reachedlock object goes out of scope
How does the mutex change state as a thread acquires and releases it, and what triggers each change?
Worked example

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?

  1. 1
    Before the call

    m is unlocked and sh is 0.

  2. 2
    Lock declaration

    The declaration of lck acquires m, so m becomes held by this thread.

  3. 3
    Update to sh

    sh = sh + 1 runs while m is held, so sh becomes 1 with exclusive access.

  4. 4
    End of function

    lck goes out of scope, so m returns to unlocked.

Result

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.

MEDIUM

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.

Thread AThread BMutex mShared data shlock m: acquiredlock m: waitsupdate shscope ends: releasedlock m: acquiredupdate shscope ends: released
When two threads both call lock on the same mutex, which one proceeds immediately, what happens to the other, and when does it proceed?

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.

Thread 1Thread 2Shared shFinal value 1read sh = 0read sh = 0write sh = 1write sh = 1expected 2
If two threads each read, increment, and write a shared variable without holding a mutex, how can their interleaved steps produce a final value different from the expected one?
Before
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.

After
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 like
    sh = sh + 1; placed before the line that declares the lock object in the same function
    Why it fails

    The update runs before the mutex is acquired, so no exclusion applies to it.

    Do this instead

    Place 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 like

    One thread locks a mutex m1 while another thread locks a separate mutex m2 and both update sh

    Why it fails

    Each thread holds a different mutex, so they are not excluded from each other.

    Do this instead

    Every 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 like

    A lock object is declared inside a nested block that closes before the update to sh

    Why it fails

    The mutex is released as soon as the scope ends, so the update runs unprotected.

    Do this instead

    Make 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.

  1. 1
    Is the shared variable touched outside any lock declaration?
  2. 2
    Do all threads that touch the variable use the same mutex object?
  3. 3
    Does the lock's scope cover every line that needs protection?

Trace Check and Practice

MEDIUM

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.

Related concepts

More concepts starting with M