Concepts / Mutexes and Critical Sections

Mutexes and Critical Sections

A mutex protects a critical section by letting only one thread hold it at a time. The trouble this article studies is not the protection itself but what happens when a thread needs more than one of these locks.

  • Programming

A Stall With No Bug in Sight

Many useful actions cannot be finished with a single shared resource. A program may need to update one data structure and then another, and each update must be protected so that no other thread changes the data in the middle of the operation. Locks, implemented as mutexes, provide that protection. The trouble begins when a task needs more than one lock at the same time, because the order in which those locks are requested can turn a correct program into one that stops making progress entirely.

Picture two people sharing one pair of scissors. Each person holds one half of the scissors and waits for the other person to give up the second half. Neither person will finish cutting. Neither did anything wrong on their own. The stall comes from the arrangement of who holds what and who is waiting for what. The same kind of arrangement can happen with threads and mutexes.

What do you think happens?

Two threads each need two mutexes, mutex1 and mutex2. Thread1 locks mutex1 and then asks for mutex2, while Thread2 locks mutex2 and then asks for mutex1, and both happen at the same moment. What happens next?

  • Both threads finish, one after the other

  • One thread finishes and the other waits briefly, then continues

  • Neither thread can move forward, and the stall never ends on its own

Reveal answer
Answer:

Neither thread can move forward, and the stall never ends on its own.

Each thread holds one mutex and is waiting for the mutex the other thread holds. Neither thread will release its mutex while it is itself stuck, so no thread can make the first move.

Why Shared Data Needs Mutexes

A mutex is a lock that protects data. Before a thread touches shared data, it acquires the mutex that guards that data. While it holds the mutex, no other thread can acquire it, so other threads wait. When the thread is done, it releases the mutex and the next waiting thread may proceed. The protected stretch of code is often called a critical section, because only one thread should be inside it at a time.

Key point

A mutex protects a critical section by letting only one thread hold it at a time. The trouble this article studies is not the protection itself but what happens when a thread needs more than one of these locks.

Tracing the Two-Thread Stall

Consider two threads and two mutexes, called mutex1 and mutex2. Thread1 acquires mutex1 and then tries to acquire mutex2. At the same moment, Thread2 acquires mutex2 and then tries to acquire mutex1. Trace the steps below and keep track of which mutex each thread owns after each one.

Thread 1Thread 2mutex1mutex21. acquire mutex12. acquire mutex23. request mutex2, blocked4. request mutex1, blocked
In what order do Thread 1 and Thread 2 take and request the two mutexes, and at which step does each one get stuck waiting?

Steps one and two are harmless. Each thread successfully takes the first lock it needs. The problem appears at steps three and four. Thread1 now holds mutex1 and is sleeping on mutex2. Thread2 holds mutex2 and is sleeping on mutex1. Each thread is waiting for the mutex the other one holds, so neither can move forward.

Why the Stall Never Ends

The arrangement above forms a loop. Each mutex points back to the thread that holds it, and each thread points to the mutex it is trying to get. Following the arrows around the loop shows why no one can move first. This closed loop of waiting is called a circular wait, and it is the core of a deadlock.

Thread 1mutex2Thread 2mutex1held bywaiting forheld bywaiting for
Which thread owns which mutex, which thread is waiting on it, and how does that relationship close into a loop?

The stall is permanent because releasing a lock is something a thread does only when it is done with the data. Thread1 would have to finish its work to release mutex1, but finishing that work requires mutex2, which Thread2 holds. Thread2 is in the same position with respect to mutex1. Because the dependency runs in a circle, no step can start the chain moving again.

Key point

A deadlock is a state in which each thread holds one resource and waits for a resource held by another thread, so none of them can proceed. Breaking any link in the circle is enough to prevent it.

How lock() Breaks the Circle

The standard library offers an operation for this situation: lock(). Instead of grabbing one mutex and then asking for the next, a thread hands all of the mutexes it needs to lock() as arguments. The operation is built on two guarantees. First, lock() proceeds only after it has acquired all of its mutex arguments. Second, it never blocks, meaning it never goes to sleep, while it is holding a mutex.

Take mutex1first locklock(mutex1,mutex2)one request for bothWait for mutex2still holding mutex1Both heldproceeds to the workNeither heldwaits without holding anythenall acquirednot all acquired
How does acquiring both mutexes in one lock() call differ from locking them one at a time, and why can the one-at-a-time version create a circular wait?

The one-at-a-time version creates the circle because a thread can hold mutex1 while it sleeps waiting for mutex2. The lock() version has no such gap. A thread either obtains everything it asked for and continues, or it does not hold any of those mutexes while it waits. Without a partial set held during a wait, the circular chain from the earlier scenario cannot form. In short, the lock() operation prevents deadlocks by acquiring all mutexes at once.

Retracing the Scenario With lock()

Worked example

The same two threads, using lock() for both mutexes

Thread1 and Thread2 both need mutex1 and mutex2. Using a single lock() call per thread that names both mutexes, what happens when the two threads run at the same time?

  1. 1
    Thread1 calls lock(mutex1, mutex2)

    Thread1 hands both mutexes to the operation at once. lock() must acquire every argument before it proceeds.

  2. 2
    Thread2 calls lock(mutex1, mutex2) at the same time

    Thread2 also requests both mutexes in one call. It cannot take mutex1 by itself while holding mutex2, because the request is handled as one unit.

  3. 3
    One thread acquires both mutexes

    Whichever thread gets both mutexes first proceeds with its work. The other thread waits without holding either mutex.

  4. 4
    The first thread releases its mutexes

    When the working thread finishes and leaves its scope, the mutexes are released. The waiting thread can now acquire both and continue.

Result

Neither thread holds one mutex while waiting for the other, so the circular wait cannot form. Both threads eventually make progress.

Releasing Mutexes With unique_lock

Acquiring locks is only half of the job. They also have to be released, or other threads wait forever. The unique_lock tool handles release. Each unique_lock object wraps a mutex, and its destructor releases that mutex when the object goes out of scope. In practical terms, when a thread leaves the block of code where the unique_lock was declared, the mutex it guards is released without the programmer writing an explicit unlock.

Enter scopeDeclare unique_lockwraps the mutexMutex acquiredWork on shared dataLeave scopeDestructor releasesmutexno explicit unlock needed
When does the unique_lock acquire the mutex, and at what point in the scope does its destructor release it?

The source ties this release behavior to the lock() operation. The destructors of the individual unique_lock objects ensure that the mutexes are released when a thread leaves the scope. This means a thread that has acquired its mutexes through lock() and wrapped each one in a unique_lock can rely on scope exit to free them. The guarantee holds when the thread leaves that scope, so the release is tied to the structure of the code rather than to a separate call the programmer must remember.

Best practice

Declare each unique_lock in the narrowest block that needs the protected data. The mutex is then released as soon as that block ends, which keeps other threads from waiting longer than necessary.

Mistakes That Bring Back the Stall

  • Common mistake

    Assuming a thread that is waiting will eventually give up its lock

    What it looks like

    Thread1 holds mutex1 and sleeps on mutex2, while Thread2 holds mutex2 and sleeps on mutex1.

    Why it fails

    A thread releases a lock only when it finishes its work, and that work needs the lock it is waiting for. The dependency runs in a circle, so neither thread can finish.

    Do this instead

    Arrange lock requests so that a waiting thread never holds a mutex another thread needs in a circle. Using lock() for the full set of mutexes removes the partial hold.

  • Common mistake

    Acquiring two mutexes one after the other in a task that another thread may also need

    What it looks like

    One function locks mutex1 and then mutex2, while another locks mutex2 and then mutex1.

    Why it fails

    Each function can hold its first mutex while waiting for the second, which is exactly the pattern that forms the circular wait.

    Do this instead

    Pass both mutexes to a single lock() call so the thread acquires them together and never sleeps on a partial set.

  • Common mistake

    Expecting unique_lock to need a separate unlock call

    Why it fails

    The mutex is released by the unique_lock destructor when the thread leaves the scope. An extra manual release step is not part of the described behavior.

    Do this instead

    Rely on the scope of the unique_lock declaration to control when the mutex is released.

Edge case

Leaving the scope is what triggers release, so a thread that leaves the scope through an exceptional exit still passes through the unique_lock destructor. The source material states the scope-exit guarantee, including the case where the thread leaves that scope, which is the reason the wrapper is useful.

Practice Tracing a Circular Wait

MEDIUM

Thread A holds mutex1 and requests mutex2. Thread B holds mutex2 and requests mutex1. Draw the ownership and waiting arrows, identify the loop, and explain in one sentence why neither thread can release its mutex first.

Hints
  • Draw each mutex with an arrow back to its holder, then draw each thread's arrow to the mutex it wants.
  • Follow the arrows around. If you return to where you started, you have found a circular wait.
  • Ask what a thread must finish before it releases its mutex, and whether that work needs the other mutex.
MEDIUM

Rewrite the same task so that Thread A and Thread B each acquire both mutexes in one lock() call. Describe what each thread does while it waits and whether any thread can end up holding only one mutex while sleeping.

Hints
  • lock() proceeds only after acquiring every argument.
  • lock() never blocks while holding a mutex.

Summary of the Stall and Its Fix

A deadlock forms when each of two threads holds one mutex and waits for the one the other holds. The waiting forms a closed loop, and because each release depends on progress that needs the lock being waited for, the stall is permanent. The lock() operation prevents this by acquiring all of its mutex arguments together, proceeding only after it has every one, and never sleeping while it holds a mutex. A unique_lock releases its mutex through its destructor when the thread leaves the scope.

At a glance

  • A deadlock occurs when threads each hold one resource and wait for a resource held by another thread, so none of them can proceed.
  • The waiting forms a circular dependency, so each thread's release depends on progress that requires the lock it is waiting for, and the stall is permanent.
  • The lock() operation acquires all of its mutex arguments together, proceeds only after getting every one, and never blocks while holding a mutex.
  • Passing all needed mutexes to one lock() call prevents the partial hold that makes a circular wait possible.
  • A unique_lock releases its mutex through its destructor when the thread leaves the scope, so release is tied to scope rather than an explicit call.

Related concepts

More concepts starting with M