Mutex Basics and lock() and unlock()
A unique_lock acquires its mutex in its constructor by calling lock() and releases it when it goes out of scope by calling unlock()
The Belief That a Mutex Guards Data
A common first reaction to mutexes is that declaring one next to some shared data makes that data safe. The mutex looks like it belongs to the data, so it seems reasonable to assume the language will hold the two together and refuse access to the data unless the right lock is held. This article tests that belief. The short answer is that the language does not do this. Understanding exactly what a unique_lock does, and what it leaves to you, is the key to using it correctly.
What do you think happens?
Suppose a mutex m protects an integer sh. One function acquires m with a unique_lock and then changes sh. A second function changes sh directly, without ever mentioning m. What does the language do with the second function?
Reveal answer
The language does not stop it. Nothing forces the second function to hold m before touching sh.
The link between m and sh is conventional. The lock system will correctly manage whichever mutex you give it, but the language does not check that the data is accessed only while the right mutex is held. Preventing that second access is the programmer's job.
What a unique_lock Does at Two Moments
A unique_lock is a lock object that manages a mutex for you. It does this at two moments. The first is construction: when the unique_lock is created, its constructor acquires the mutex, which it does through a call to m.lock(). The second is the end of its scope: when the unique_lock goes out of scope, it releases the mutex, through a call to m.unlock().
You do not write either call yourself. The lock object does both, and the timing of the release is set by where the object's lifetime ends. This is why the placement of the unique_lock's declaration matters: a lock declared near the top of a block holds the mutex for the whole block, while one declared in a narrower block holds it only for that narrower region.
Free and Held: The Two States of a Mutex
It helps to think of a mutex as being in one of two states at any moment: free, or held. A free mutex becomes held when a unique_lock is constructed. Its owner gives it back when the lock leaves scope, and at that point it is free again. The diagram below reads as a cycle that repeats each time a lock is taken and released.
Waiting for a Held Mutex
The waiting behavior is the reason the mutex exists. If another thread has already acquired the mutex, the thread that asks for it waits, which the source calls blocking, until the holder completes its access to the shared data. Blocking is not a failure. It is the mechanism that keeps two threads from working on the shared data at the same time.
When the unique_lock releases the mutex, the threads that were waiting for it resume executing, which the source describes as being woken up. The sequence below follows one holder and one waiter. Thread A holds the mutex. Thread B asks for it, is blocked, and only continues after A's release.
Why the Pairing Stays With the Programmer
The source states plainly that the correspondence between a mutex and the shared data it protects is conventional. The programmer has to know which mutex is supposed to correspond to which data. The source also admits that this is error-prone, and it describes language features that try to make the pairing clearer. That is why the pairing is worth studying as its own topic: the lock system will correctly handle whatever mutex you give it, but it cannot tell whether you gave it the right one.
The source gives a concrete case. For a Record called rec, the member rec.rm is a mutex that you are supposed to acquire before accessing the other data of rec. Nothing about the record forces that rule. The rule lives in the programmer's understanding of how the record is meant to be used, and a name or a comment is the main way a later reader learns about it.
Worked Example: Two Functions Touching One Value
Tracing a mutex protecting an integer
A mutex m protects an integer sh. Function F1 acquires m with a unique_lock and then changes sh. Function F2 changes sh directly without mentioning m. Two threads run F1 and F2 at about the same time. What guarantees does the lock system give, and what does it leave open?
- 1Step 1: F1 constructs its unique_lock
The constructor calls
m.lock(). If m was free, it is now held by F1's thread, and F1 can change sh safely with respect to other threads that also lock m. - 2Step 2: F2 runs without locking
F2 never asks for m, so the lock system never sees its access. It can change sh while F1 holds m, because the language does not link sh to m.
- 3Step 3: F1 leaves scope
When F1's
unique_lockgoes out of scope, it callsm.unlock(), returning m to the free state. Any thread waiting on m is woken up. - 4Step 4: Judge the result
The lock system correctly serialized the accesses that went through m. The unguarded access in F2 was not prevented, because the pairing between m and sh exists only in the programmer's design.
The mutex only protects the data when every access to that data goes through the same mutex. The fix is in the code's design, not in the lock itself.
Mistakes With Scope and Pairing
The source notes that mistakes usually come from unguarded accesses or from holding a lock longer than needed. That makes both scope and naming important. The items below cover the errors that follow most directly from the two behaviors described in this article.
Common mistake Assuming that declaring a mutex next to the data makes the data safe
What it looks likeA mutex m and an integer sh are declared together, and some functions change sh without locking m.
Why it failsThe language does not enforce the link between a mutex and the data it protects, so unguarded accesses are still allowed.
Do this insteadMake every access to the protected data go through the same mutex, and document which mutex guards which data.
Common mistake Holding a unique_lock for longer than the work requires
What it looks likeA unique_lock is declared at the top of a function, but the protected access is only one line near the bottom.
Why it failsThe mutex stays held until the lock leaves scope, which keeps other waiting threads blocked for longer than necessary.
Do this insteadNarrow the scope of the unique_lock so it covers only the access it protects.
Common mistake Relying on a name or comment alone to show which mutex guards which data
What it looks likeA record has a member rm that is supposed to be locked before other members are read, but nothing in the type says so.
Why it failsA later reader can only learn the rule from naming or comments, and those can be missed or go out of date.
Do this insteadUse clear naming and written comments that state the pairing, and check the pairing during code review.
Practice: Predicting Waiting and Release
Thread A constructs a unique_lock on mutex m and stays inside that scope for a while. Thread B then tries to construct its own unique_lock on m. Describe where Thread B waits, what state the mutex is in while it waits, and what event lets Thread B continue.
Hints
- Ask which call in B's construction would block, given that A already holds m.
- Think about what the mutex's state is between A's construction and A's scope exit.
- The source says the release wakes waiting threads. Which event is the release?
Given a record with a mutex member rm and a data member that rm is meant to guard, list the two checks a reviewer should make to confirm that every access to the data is guarded by rm.
Hints
- Consider both reading and writing the data.
- Consider every function, not just the ones the author remembers.
Key Takeaways
- A
unique_lockcallsm.lock()in its constructor, acquiring the mutex, and callsm.unlock()when it goes out of scope, releasing it. - A mutex is either free or held. A thread that finds it held blocks, and the release wakes the waiting thread.
- The link between a mutex and the data it protects is conventional. The language does not enforce that the right mutex is held before the data is accessed.
- Errors come from unguarded accesses and from holding a lock longer than needed, so both the scope of the lock and the naming of the mutex matter.