26. Lock Ordering
In a low-latency matching engine, two shared structures are guarded by separate mutexes. Different code paths may touch them in varying orders under heavy contention, risking stalls that explode tail latency.
struct Book {
std::mutex m1, m2;
void f(){ std::lock_guard<std::mutex> a(m1); std::lock_guard<std::mutex> b(m2); }
void g(){ std::lock_guard<std::mutex> b(m2); std::lock_guard<std::mutex> a(m1); }
};
Part 1.
What concurrency risk exists in f/g under load, and how would you rewrite this to eliminate it with minimal overhead?
Part 2.
(1) When should std::scoped_lock be preferred over lock_guard?
(2) How to acquire multiple mutexes safely with std::lock and adopt_lock?
(3) What costs does unique_lock add versus lock_guard?
(4) How does mutex ownership transfer interact with RAII and exceptions?
(5) How to enforce a global lock order in large codebases?
Answer
Answer (Part 1)
f and g can deadlock via lock-order inversion when two threads acquire m1 then m2 versus m2 then m1. Fix by acquiring both in a single operation with std::scoped_lock l(m1, m2); in both functions, or use std::lock(m1, m2); followed by std::lock_guard<std::mutex> l1(m1, std::adopt_lock); and l2(m2, std::adopt_lock); to standardize ordering and avoid deadlock without extra syscalls.
Answer (Part 2)
(1) Prefer std::scoped_lock when locking multiple mutexes; it uses std::lock to avoid deadlock. For one mutex, it’s equivalent to lock_guard.
(2) Call std::lock(m1, m2) then construct guards with adopt_lock. This ensures both are acquired without deadlock and transfers ownership to RAII.
(3) unique_lock stores ownership state and supports unlock/relock/try, adding size and branches. lock_guard is smaller, fixed-cost, best for simple scopes.
(4) unique_lock is movable, so ownership can transfer across scopes safely. RAII still guarantees release on destruction, even during exceptions.
(5) Define a consistent key-based order and always lock ascending. Enforce with wrappers, code review, debug assertions, and static analysis.