Timed lock algorithms for multiple lockables

Revision History

R1

Modifications after 2025-11_Kona:SG1-P3832R0 meeting:

Abstract

C++11 introduced std::lock and std::try_lock (and C++17 introduced std::scoped_lock) to simplify deadlock-free acquisition of multiple lockables. These algorithms support BasicLockable and Lockable objects, but there is currently no facility for timed acquisition of multiple TimedLockable objects.

Users who require timeout-based locking of multiple mutexes must implement their own deadlock-avoidance algorithm, typically via try_lock(), unlock(), and retry. This is error-prone, verbose, and inconsistent with the existing standard library facilities.

This paper proposes two new algorithms:

template <class Clock, class Duration, class... Ls>
int try_lock_until(const std::chrono::time_point<Clock, Duration>& tp, Ls&... ls);

template <class Rep, class Period, class... Ls>
int try_lock_for(const std::chrono::duration<Rep, Period>& rel_time, Ls&... ls);

These extend the std::lock family of functions to timed lockables, enabling consistent and safe use of multiple timed mutexes.

Contents

  1. Revision History
  2. Abstract
  3. Impact on the Standard
  4. Design Rationale
  5. Proposed Wording
  6. Example
  7. Implementation Experience
  8. Acknowledgments
  9. References

3. Impact on the Standard

4. Design Rationale

5. Proposed Wording

The following changes are relative to N5014.

In 32.6.6, Generic locking algorithms [thread.lock.algorithm], after point 5:

template <class Clock, class Duration, class... Ls>
int try_lock_until(const chrono::time_point<Clock, Duration>& abs_time, Ls&... ls);
6
Preconditions:
Each template parameter type in Ls meets the Cpp17TimedLockable requirements.
7
Effects:
Attempts to obtain ownership of all arguments via repeated calls to try_lock_until(), try_lock_for(), try_lock() or unlock() on each argument. The sequence of calls does not result in deadlock. No call to try_lock_for() or try_lock_until() is made while holding a lock on any argument, but the sequence is otherwise unspecified. If all locks are acquired before abs_time has passed, returns -1. If the time point abs_time is reached before all locks are acquired, releases any locks it holds and returns the index of the lockable for which try_lock_until() or try_lock_for() failed due to abs_time being reached. If a call to try_lock_until(), try_lock_for() or try_lock() throws an exception, unlock() is called on any object locked by this algorithm prior to the exception, and the exception is rethrown.
An implementation should ensure that try_lock_until() does not consistently return ≥ 0 in the absence of contending mutex acquisitions.
8
Returns:
-1 if all locks were obtained, otherwise the index of the lockable for which try_lock_until() or try_lock_for() failed due to abs_time being reached.
9
Throws:
Any exception thrown by the lockable try_lock_until(), try_lock_for(), or try_lock() functions.
template <class Rep, class Period, class... Ls>
int try_lock_for(const chrono::duration<Rep, Period>& rel_time, Ls&... ls);
9
Preconditions:
Each template parameter type in Ls meets the Cpp17TimedLockable requirements.
10
Effects:
Equivalent to:
return try_lock_until(chrono::steady_clock::now() + rel_time, ls...);

6. Example

std::timed_mutex m1, m2;
if (std::try_lock_for(100ms, m1, m2) == -1) {
    // success
    std::scoped_lock sl(std::adopt_lock, m1, m2);
    // ...
} else {
    // failed to acquire within timeout
}

7. Implementation Experience

Existing implementations of std::lock already use a deadlock-avoidance algorithm. Using the gcc implementation as an example, instead of locking one with m.lock() and using std::try_lock() on the rest, the algorithm could start locking one with m.try_lock_until(tp). Example at Compiler Explorer

A reference implementation is available at github.com/bemanproject/timed_lock_alg.

8. Acknowledgments

The std-proposals mailing list:

Reference implementation:

9. References