std::multi_lockThis paper proposes std::multi_lock, a RAII class
template that combines the functionality of
std::unique_lock and std::scoped_lock. Unlike
std::scoped_lock, which provides only basic RAII semantics,
std::multi_lock offers the full flexibility of
std::unique_lock (deferred locking, try-lock operations,
timed locking, and ownership transfer) while supporting multiple mutexes
simultaneously.
The C++ standard library provides several mutex wrapper classes:
std::lock_guard: Simple RAII wrapper for a single
mutexstd::unique_lock: Flexible RAII wrapper for a single
mutex with deferred locking, try-lock, and ownership transferstd::scoped_lock: RAII wrapper for multiple mutexes
with deadlock avoidanceHowever, there is no facility that combines the flexibility of
std::unique_lock with the multi-mutex capabilities of
std::scoped_lock. While std::scoped_lock
provides excellent RAII semantics for multiple mutexes, it always locks
immediately at construction and cannot be used in situations
requiring:
These are common patterns when working with a single mutex using
std::unique_lock, but developers currently have no
equivalent option for multiple mutexes. This gap forces developers to
choose between:
std::scoped_lock and restructuring code to avoid
deferred/timed locking scenariosstd::unique_lock objects manually,
which is verbose and error-proneThe following table illustrates how multi_lock completes
the family of mutex wrappers:
| One mutex | Zero or more mutexes | |
|---|---|---|
| Always owning | lock_guard |
scoped_lock |
| Flexible (*) | unique_lock |
multi_lock |
(*) Supports deferred locking, time-constrained attempts at locking, recursive locking, and transfer of lock ownership.
This paper proposes std::multi_lock<Ms...>, a
variadic class template that:
unique_lock in this regard.Example 1: Deferred locking of multiple mutexes
std::mutex m1, m2;
std::multi_lock lock(std::defer_lock, m1, m2);
// ... prepare work ...
if (condition) {
lock.lock(); // Deadlock-safe locking
// critical section
}Example 2: Timed locking with timeout
std::multi_lock lock(100ms, m1, m2);
if (lock) {
// Successfully acquired all locks within timeout
// critical section
}Example 3: Conditional locking with manual control
std::multi_lock lock(std::try_to_lock, m1, m2);
if (!lock) {
// Could not acquire all locks, handle gracefully
return;
}Example 4: Replacing a scoped_lock with deferred
and timed locking
Before:
std::mutex m1, m2;
//...
std::scoped_lock lock(m1, m2);
// critical sectionAfter:
std::timed_mutex m1, m2;
//...
std::multi_lock lock(std::defer_lock, m1, m2);
for(int rv; (rv = lock.try_lock_for(100ms)) != -1;) {
// log something or take some other action
}
// critical sectionThis proposal is a pure library extension. It adds a new class
template std::multi_lock to the <mutex>
header and updates related sections of the standard to reference it.
There are no changes to the core language and no breaking changes to
existing code.
Following std::try_lock, the try_lock(),
try_lock_for(), and try_lock_until() member
functions return int: - -1 on success (all
locks acquired) - Otherwise, the 0-based index of the mutex that failed
to lock
This maintains consistency with existing standard library facilities.
The standard library currently lacks std::try_lock_for
and std::try_lock_until functions for multiple mutexes. P3832 proposes to add these free
functions. This proposal includes corresponding
std::multi_lock member functions that provide this
functionality, possibly by using the two proposed free functions.
std::multi_lock provides the following exception safety
guarantees:
try_lock, try_lock_for, or
try_lock_until) leave all mutexes unlocked. If any mutex
fails to lock, all previously acquired locks in that operation are
released before returning.multi_lock(multi_lock&&) and
operator=(multi_lock&&)), swap(), and
release() never throw exceptions.The destructor provides the basic guarantee: if
owns_lock() is true, it calls
unlock(), which may throw but ensures all mutexes are
unlocked before propagating the exception.
When locking multiple mutexes, std::multi_lock must
avoid deadlock. The implementation can use a similar deadlock-avoidance
algorithm as std::lock() for multiple mutexes:
lock(): It can use
std::lock(*get<Is>(pm)...) which locks all mutexes
via a sequence of calls that does not result in deadlock, but is
otherwise unspecified.try_lock(): Attempts to lock mutexes in order and
backs off if any lock fails. It can use
std::try_lock(*get<Is>(pm)...) for this purpose.try_lock_until() and try_lock_for():
It can use an algorithm similar to std::lock() but with
timed operations, as proposed in P3832.<mutex>
synopsisnamespace std {
template<class... MutexTypes>
class multi_lock;
template<class... MutexTypes>
void swap(multi_lock<MutexTypes...>& x, multi_lock<MutexTypes...>& y) noexcept;
}multi_locknamespace std {
template<class... MutexTypes>
class multi_lock {
public:
using mutex_type = tuple<MutexTypes*...>;
// Constructors
multi_lock() noexcept;
explicit multi_lock(MutexTypes&... m);
multi_lock(defer_lock_t, MutexTypes&... m) noexcept;
multi_lock(try_to_lock_t, MutexTypes&... m);
multi_lock(adopt_lock_t, MutexTypes&... m) noexcept;
template<class Rep, class Period>
multi_lock(const chrono::duration<Rep, Period>& timeout_duration, MutexTypes&... m);
template<class Clock, class Duration>
multi_lock(const chrono::time_point<Clock, Duration>& timeout_time, MutexTypes&... m);
// Destructor
~multi_lock();
// Move operations
multi_lock(multi_lock&& other) noexcept;
multi_lock& operator=(multi_lock&& other) noexcept;
// Deleted copy operations
multi_lock(const multi_lock&) = delete;
multi_lock& operator=(const multi_lock&) = delete;
// Locking operations
void lock();
int try_lock();
template<class Rep, class Period>
int try_lock_for(const chrono::duration<Rep, Period>& timeout_duration);
template<class Clock, class Duration>
int try_lock_until(const chrono::time_point<Clock, Duration>& timeout_time);
void unlock();
// Modifiers
void swap(multi_lock& other) noexcept;
mutex_type release() noexcept;
// Observers
mutex_type mutex() const noexcept;
bool owns_lock() const noexcept;
explicit operator bool() const noexcept;
private:
mutex_type pm; // exposition only
bool owns; // exposition only
};
template<class... MutexTypes>
void swap(multi_lock<MutexTypes...>& x, multi_lock<MutexTypes...>& y) noexcept;
}An alternative design would use a container-based interface:
multi_lock(std::vector<std::mutex*> mutexes);Or with std::span:
template<class Mutex>
multi_lock(std::span<Mutex*> mutexes);Rejected: Both approaches require all mutexes to be
of the same type. While std::span avoids ownership concerns
and could be non-owning, it still cannot mix different mutex types
(e.g., std::mutex and std::timed_mutex in the
same lock). The variadic template approach maintains type safety, allows
heterogeneous mutex types, and enables compile-time optimizations that
would not be possible with a runtime container. Additionally, it is
probably preferable to keep the interface similar to that of
unique_lock and scoped_lock to complete the
family of mutex wrapper classes with a consistent design.
A complete implementation is available at github.com/bemanproject/timed_lock_alg. The implementation has been tested with multiple mutex types and demonstrates that the design is implementable and practical.
The following changes are relative to N5014.
Modify paragraph 3:
The standard library templates unique_lock (32.6.5.4),
shared_lock (32.6.5.5), multi_lock
(32.6.X), scoped_lock (32.6.5.3),
lock_guard (32.6.5.2), lock,
try_lock (32.6.6), and condition_variable_any
(32.7.5) all operate on user-supplied lockable objects. The
Cpp17BasicLockable requirements, the Cpp17Lockable
requirements, the Cpp17TimedLockable requirements, the
Cpp17SharedLockable requirements, and the
Cpp17SharedTimedLockable requirements list the requirements
imposed by these library types in order to acquire or release ownership
of a lock by a given execution agent.
<mutex> synopsis [thread.mutex.syn]Add to the header synopsis after the declaration of class template
scoped_lock:
// 32.6.X, class template multi_lock
template<class... MutexTypes>
class multi_lock;
template<class... MutexTypes>
void swap(multi_lock<MutexTypes...>& x, multi_lock<MutexTypes...>& y) noexcept;multi_lock [thread.lock.multi]namespace std {
template<class... MutexTypes>
class multi_lock {
public:
using mutex_type = tuple<MutexTypes*...>;
// 32.6.X.2, construct/copy/destroy
multi_lock() noexcept;
explicit multi_lock(MutexTypes&... m);
multi_lock(defer_lock_t, MutexTypes&... m) noexcept;
multi_lock(try_to_lock_t, MutexTypes&... m);
multi_lock(adopt_lock_t, MutexTypes&... m) noexcept;
template<class Rep, class Period>
multi_lock(const chrono::duration<Rep, Period>& rel_time, MutexTypes&... m);
template<class Clock, class Duration>
multi_lock(const chrono::time_point<Clock, Duration>& abs_time, MutexTypes&... m);
~multi_lock();
multi_lock(const multi_lock&) = delete;
multi_lock& operator=(const multi_lock&) = delete;
multi_lock(multi_lock&& u) noexcept;
multi_lock& operator=(multi_lock&& u) noexcept;
// 32.6.X.3, locking
void lock();
int try_lock();
template<class Rep, class Period>
int try_lock_for(const chrono::duration<Rep, Period>& rel_time);
template<class Clock, class Duration>
int try_lock_until(const chrono::time_point<Clock, Duration>& abs_time);
void unlock();
// 32.6.X.4, modifiers
void swap(multi_lock& u) noexcept;
mutex_type release() noexcept;
// 32.6.X.5, observers
bool owns_lock() const noexcept;
explicit operator bool() const noexcept;
mutex_type mutex() const noexcept;
private:
mutex_type pm; // exposition only
bool owns; // exposition only
};
template<class... MutexTypes>
void swap(multi_lock<MutexTypes...>& x, multi_lock<MutexTypes...>& y) noexcept;
}1 A multi_lock controls the ownership
of lockable objects within a scope. Ownership of the lockable objects
may be acquired at construction or after construction, and may be
transferred, after acquisition, to another multi_lock
object. multi_lock specializations are not copyable but are
movable. The behavior of a program is undefined if any contained pointer
in pm is not null and the lockable object pointed to does
not exist for the entire remaining lifetime (6.8.4) of the
multi_lock object. All types in the template parameter pack
MutexTypes shall meet the Cpp17BasicLockable
requirements (32.2.5.2).
2 [Note 1:
multi_lock<MutexTypes...> meets the
Cpp17BasicLockable requirements. If all types in
MutexTypes meet the Cpp17Lockable requirements
(32.2.5.3), multi_lock<MutexTypes...> also meets the
Cpp17Lockable requirements; if all types in
MutexTypes meet the Cpp17TimedLockable
requirements (32.2.5.4), multi_lock<MutexTypes...>
also meets the Cpp17TimedLockable requirements. — end
note]
multi_lock() noexcept;pm == tuple<MutexTypes*...>{} and
owns == false.explicit multi_lock(MutexTypes&... m);sizeof...(MutexTypes) > 0 is true.sizeof...(MutexTypes) does not equal 1, all types in
MutexTypes meet the Cpp17Lockable requirements
(32.2.5.3).pm with
addressof(m).... Then calls lock().pm == tuple<MutexTypes*...>{addressof(m)...} and
owns == true.multi_lock(defer_lock_t, MutexTypes&... m) noexcept;pm with
addressof(m)....pm == tuple<MutexTypes*...>{addressof(m)...} and
owns == false.multi_lock(try_to_lock_t, MutexTypes&... m);MutexTypes meet the Cpp17Lockable requirements
(32.2.5.3).pm with
addressof(m).... Then calls try_lock().pm == tuple<MutexTypes*...>{addressof(m)...} and
owns is true if res equals -1,
otherwise false, where res is the value
returned by try_lock().multi_lock(adopt_lock_t, MutexTypes&... m) noexcept;m.pm
with addressof(m)....pm == tuple<MutexTypes*...>{addressof(m)...} and
owns == true.template<class Rep, class Period>
multi_lock(const chrono::duration<Rep, Period>& rel_time, MutexTypes&... m);MutexTypes meet the Cpp17TimedLockable
requirements (32.2.5.4).pm
with addressof(m).... Then calls
try_lock_for(rel_time).pm == tuple<MutexTypes*...>{addressof(m)...} and
owns is true if res equals -1,
otherwise false, where res is the value
returned by try_lock_for(rel_time).template<class Clock, class Duration>
multi_lock(const chrono::time_point<Clock, Duration>& abs_time, MutexTypes&... m);MutexTypes meet the Cpp17TimedLockable
requirements (32.2.5.4).pm
with addressof(m).... Then calls
try_lock_until(abs_time).pm == tuple<MutexTypes*...>{addressof(m)...} and
owns is true if res equals -1,
otherwise false, where res is the value
returned by try_lock_until(abs_time).multi_lock(multi_lock&& u) noexcept;pm == u_p.pm and owns == u_p.owns (where
u_p is the state of u just prior to this
construction), u.pm == tuple<MutexTypes*...>{} and
u.owns == false.multi_lock& operator=(multi_lock&& u) noexcept;multi_lock(std::move(u)).swap(*this).*this.~multi_lock();owns is
true, calls unlock().void lock();sizeof...(MutexTypes) does not equal 1, all types in
MutexTypes meet the Cpp17Lockable requirements
(32.2.5.3).sizeof...(MutexTypes) equals 0, no effects. Otherwise, if
sizeof...(MutexTypes) equals 1, calls
get<0>(pm)->lock(). Otherwise, calls
lock(*get<Is>(pm)...) where Is is
0, 1, ..., sizeof...(MutexTypes)-1.owns == true.lock() functions. system_error when an
exception is required (32.2.2).(5.1)— operation_not_permitted — if any pointer in
pm is null.
(5.2)— resource_deadlock_would_occur — if on entry
owns is true.
int try_lock();MutexTypes meet the Cpp17Lockable requirements
(32.2.5.3).sizeof...(MutexTypes) equals 0, returns -1. Otherwise, if
sizeof...(MutexTypes) equals 1, calls
get<0>(pm)->try_lock() and returns -1 if
successful, 0 otherwise. Otherwise, calls
try_lock(*get<Is>(pm)...) where Is is
0, 1, ..., sizeof...(MutexTypes)-1.owns is
true if the return value equals -1, otherwise
false.-1 if all locks
were acquired, otherwise the 0-based index of the mutex that failed to
lock.try_lock() functions. system_error when
an exception is required (32.2.2).(11.1)— operation_not_permitted — if any pointer in
pm is null.
(11.2)— resource_deadlock_would_occur — if on entry
owns is true.
template<class Rep, class Period>
int try_lock_for(const chrono::duration<Rep, Period>& rel_time);MutexTypes meet the Cpp17TimedLockable
requirements (32.2.5.4).return try_lock_until(chrono::steady_clock::now() + rel_time);template<class Clock, class Duration>
int try_lock_until(const chrono::time_point<Clock, Duration>& abs_time);MutexTypes meet the Cpp17TimedLockable
requirements (32.2.5.4).abs_time. If
sizeof...(MutexTypes) equals 0, returns -1. Otherwise, if
sizeof...(MutexTypes) equals 1, calls
get<0>(pm)->try_lock_until(abs_time) and returns
-1 if successful, 0 otherwise. Otherwise, uses an algorithm similar to
lock() but with timed operations respecting
abs_time.owns is
true if the return value equals -1, otherwise
false.-1 if all locks
were acquired, otherwise the 0-based index of the mutex that failed to
lock before the timeout.try_lock_until() functions. system_error
when an exception is required (32.2.2).(19.1)— operation_not_permitted — if any pointer in
pm is null.
(19.2)— resource_deadlock_would_occur — if on entry
owns is true.
void unlock();0,sizeof...(MutexTypes)),
get<i>(pm)->unlock().owns == false.system_error when
an exception is required (32.2.2).(23.1)— operation_not_permitted — if on entry
owns is false.
void swap(multi_lock& u) noexcept;*this and u.mutex_type release() noexcept;pm == tuple<MutexTypes*...>{} and
owns == false.pm.template<class... MutexTypes>
void swap(multi_lock<MutexTypes...>& x, multi_lock<MutexTypes...>& y) noexcept;x.swap(y).bool owns_lock() const noexcept;owns.explicit operator bool() const noexcept;owns.mutex_type mutex() const noexcept;pm.