Concepts / Mutex Basics and lock() and unlock()

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()

  • Programming

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

unique_lockconstructedcalls m.lock()Mutex heldprotected access runsScope endsunique_lock goes out ofscopeMutex releasedcalls m.unlock()acquiredblock endsrelease
In what order do the constructor, the protected code, and the scope exit run, and at which point is the mutex actually held?

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.

Freeno ownerHeldowned by one threadunique_lock constructedunique_lock leaves scope
How does the mutex change state as threads construct and leave unique_lock objects, and what triggers each transition?

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.

Thread Aholds the lockMutex mshared lockThread Bwaiting1. lock() acquires2. lock() finds it held and blocks3. unlock() on scope exit4. release wakes B
When Thread B calls lock() while Thread A holds the mutex, where does B wait, and what happens to B when A releases the mutex?

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

Worked example

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?

  1. 1
    Step 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.

  2. 2
    Step 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.

  3. 3
    Step 3: F1 leaves scope

    When F1's unique_lock goes out of scope, it calls m.unlock(), returning m to the free state. Any thread waiting on m is woken up.

  4. 4
    Step 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.

Result

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 like

    A mutex m and an integer sh are declared together, and some functions change sh without locking m.

    Why it fails

    The language does not enforce the link between a mutex and the data it protects, so unguarded accesses are still allowed.

    Do this instead

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

    A unique_lock is declared at the top of a function, but the protected access is only one line near the bottom.

    Why it fails

    The mutex stays held until the lock leaves scope, which keeps other waiting threads blocked for longer than necessary.

    Do this instead

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

    A 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 fails

    A later reader can only learn the rule from naming or comments, and those can be missed or go out of date.

    Do this instead

    Use clear naming and written comments that state the pairing, and check the pairing during code review.

Practice: Predicting Waiting and Release

EASY

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?
MEDIUM

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_lock calls m.lock() in its constructor, acquiring the mutex, and calls m.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.

Related concepts

More concepts starting with M