Why Use std::scoped_lock Over std::lock in C++
Introduced in C++17, std::scoped_lock provides a modern,
RAII-based mechanism for acquiring multiple mutexes simultaneously
without risking deadlocks. While the legacy std::lock
function also avoids deadlocks across multiple locks, it merely acquires
the locks without managing their lifetimes, requiring additional
boilerplate to ensure they are properly released. This article outlines
the key advantages of using std::scoped_lock over
std::lock, highlighting improvements in exception safety,
boilerplate reduction, and overall code clarity.
Built-in RAII and Automatic Unlocking
The most significant advantage of std::scoped_lock is
its adherence to the Resource Acquisition Is Initialization (RAII)
idiom.
When using std::lock, the function locks the supplied
mutexes, but it does not unlock them. To prevent resource leaks, you
must manually bind each locked mutex to an RAII wrapper like
std::lock_guard or std::unique_lock using the
std::adopt_lock tag:
// Legacy approach with std::lock
std::lock(mutexA, mutexB);
std::lock_guard<std::mutex> lockA(mutexA, std::adopt_lock);
std::lock_guard<std::mutex> lockB(mutexB, std::adopt_lock);In contrast, std::scoped_lock handles both acquisition
and release within a single object:
// Modern approach with std::scoped_lock
std::scoped_lock lock(mutexA, mutexB);When lock leaves scope, both mutexA and
mutexB are automatically unlocked in reverse order of
destruction, eliminating the risk of dangling locks.
Robust Exception Safety
Using std::lock introduces a subtle exception-safety
vulnerability. If an exception occurs after std::lock
succeeds but before all corresponding std::lock_guard
objects are instantiated, the mutexes remain locked permanently, leading
to application-wide deadlocks.
std::scoped_lock eliminates this window entirely. Its
constructor guarantees that if an exception is thrown during lock
acquisition, any mutexes that were already locked during the process are
automatically released before the exception propagates.
Reduced Boilerplate with Class Template Argument Deduction (CTAD)
std::scoped_lock leverages variadic templates and C++17
Class Template Argument Deduction (CTAD). Developers do not need to
explicitly specify the types of mutexes being locked.
Instead of declaring multiple verbose wrapper lines, a single
statement locks an arbitrary number of mutex types—even heterogeneous
ones, such as a std::mutex alongside a
std::shared_mutex or a custom BasicLockable type:
std::scoped_lock lock(standardMutex, sharedMutex, customMutex);Unified Mutex Management
std::scoped_lock is designed as a direct, strictly
superior replacement for std::lock_guard. It functions
equally well with a single mutex:
std::scoped_lock lock(singleMutex); // Fully replaces std::lock_guardWhen given a single mutex, std::scoped_lock avoids the
overhead of the multi-lock deadlock avoidance algorithm and simply
invokes lock() on the single object. By standardizing on
std::scoped_lock, codebases can maintain a single,
consistent locking pattern for both single-lock and multi-lock critical
sections.