liveBook · Manning

Lock hierarchy

As explained in C++ Concurrency in Action, Second Edition

A convention in which threads acquire multiple locks in a consistent order to prevent circular waiting.

A lock hierarchy is a rule for acquiring multiple locks in a consistent order. It prevents the circular wait in which one thread holds mutex A while waiting for B and another holds B while waiting for A. Use a fixed order when locks must be acquired separately; when they can be acquired together, `std::lock` is another way to avoid deadlock. section 3.2.5 section 3.2.4

The order must hold across every path that acquires the locks:

  • A parameter-based rule can fail. If two threads swap the same two objects with their arguments reversed, each may lock a different object first. section 3.2.4
  • Per-node locking makes the rule harder to maintain. Deleting a linked-list node requires locks on that node and both neighbors; traversals in opposite directions can acquire node locks in conflicting orders. section 3.2.5
  • Calls made while holding a lock also matter. User-supplied code may acquire another lock, so its behavior must respect the ordering rule or be constrained. section 3.2.5

Numbered hierarchy levels

Assign a hierarchy value to each mutex and acquire higher-valued locks before lower-valued ones. With values 10000 and 5000, a function holding the 10000-level mutex can call one that locks the 5000-level mutex. A `hierarchical_mutex` can check the current thread’s level before locking and throw `std::logic_error` on a violation. section 3.2.5

Listing 3.8. A simple hierarchical mutex
class hierarchical_mutex
{
    std::mutex internal_mutex;
    unsigned long const hierarchy_value;
    unsigned long previous_hierarchy_value;
    static thread_local unsigned long this_thread_hierarchy_value;   #1
    void check_for_hierarchy_violation()
    {
        if(this_thread_hierarchy_value <= hierarchy_value)           #2
        {
            throw std::logic_error("mutex hierarchy violated");
        }
    }
    void update_hierarchy_value()
    {
        previous_hierarchy_value=this_thread_hierarchy_value;        #3
        this_thread_hierarchy_value=hierarchy_value;
    }
public:
    explicit hierarchical_mutex(unsigned long value):
        hierarchy_value(value),
        previous_hierarchy_value(0)
    {}
    void lock()
    {
        check_for_hierarchy_violation();
        internal_mutex.lock();                                       #4
        update_hierarchy_value();                                    #5
    }
    void unlock()
    {
        if(this_thread_hierarchy_value!=hierarchy_value)
            throw std::logic_error("mutex hierarchy violated");      #9
        this_thread_hierarchy_value=previous_hierarchy_value;        #6
        internal_mutex.unlock();
    }
    bool try_lock()
    {
        check_for_hierarchy_violation();
        if(!internal_mutex.try_lock())                               #7
            return false;
        update_hierarchy_value();
        return true;
    }
};
thread_local unsigned long
    hierarchical_mutex::this_thread_hierarchy_value(ULONG_MAX);      #8


!@%STYLE%@!
{"css":"{\"css\": \"font-weight: bold;\"}","target":"[[{\"line\":5,\"ch\":69},{\"line\":5,\"ch\":71}],[{\"line\":8,\"ch\":69},{\"line\":8,\"ch\":71}],[{\"line\":15,\"ch\":69},{\"line\":15,\"ch\":71}],[{\"line\":26,\"ch\":69},{\"line\":26,\"ch\":71}],[{\"line\":27,\"ch\":69},{\"line\":27,\"ch\":71}],[{\"line\":32,\"ch\":69},{\"line\":32,\"ch\":71}],[{\"line\":33,\"ch\":69},{\"line\":33,\"ch\":71}],[{\"line\":39,\"ch\":69},{\"line\":39,\"ch\":71}],[{\"line\":46,\"ch\":69},{\"line\":46,\"ch\":71}]]"}
!@%STYLE%@!

The hierarchical mutex listing shows the per-thread hierarchy value, the pre-lock check, and the update made after a permitted acquisition. section 3.2.5

How C++ Concurrency in Action, Second Edition covers it

How deep it goes
Works through ordering pitfalls and a numbered hierarchical mutex with a per-thread violation check.
Examples
Uses reversed swap arguments, opposite linked-list traversals, and mutexes assigned hierarchy values of 10000 and 5000.
What it assumes
Assumes familiarity with threads and mutex locking while motivating deadlock through a two-thread, two-mutex example.
What you can do after
Practice/perform consistent lock acquisition and check whether calls made while holding a lock preserve that order.
How it connects
Relates lock ordering to deadlock, mutexes, linked-list traversal, nested locks, and `std::lock`.

Related concepts

This explainer was generated by Manning's knowledge graph from the text of C++ Concurrency in Action, Second Edition. The section links point to the parts of the book it draws on.