2135. Unclear requirement for exceptions thrown in condition_variable::wait()

Section: 33.5.3 [thread.condition.condvar], 33.5.4 [thread.condition.condvarany] Status: C++14 Submitter: Pete Becker Opened: 2012-03-06 Last modified: 2015-09-28

Priority: Not Prioritized

View all other issues in [thread.condition.condvar].

View all issues with C++14 status.

Discussion:

condition_varible::wait() (and, presumably, condition_variable_any::wait(), although I haven't looked at it) says that it calls lock.unlock(), and if condition_variable::wait() exits by an exception it calls lock.lock() on the way out. But if the initial call to lock.unlock() threw an exception, does it make sense to call lock.lock()? We simply don't know the state of that lock object, and it's probably better not to touch it.

That aside, once the wait() call has been unblocked, it calls lock.lock(). If lock.lock() throws an exception, what happens? The requirement is:

If the function exits via an exception, lock.lock() shall be called prior to exiting the function scope.

That can be read in two different ways. One way is as if it said "lock.lock() shall have been called …", i.e. the original, failed, call to lock.lock() is all that's required. But a more natural reading is that wait has to call lock.lock() again, even though it already failed.

I think this wording suffers from being too general. There are two possible exception sources: the initial call to lock.unlock() and the final call to lock.lock(). Each one should have its own requirement. Lumping them together muddles things.

[2012, Portland: move to Open]

Pablo: unlock failing is easy -- the call leaves it locked. The second case, trying to lock fails -- what can you do? This is an odd state as we had it locked before was called wait. Maybe we should call terminate as we cannot meet the post-conditions. We could throw a different exception.

Hans: calling terminate makes sense as we're likely to call it soon anyway and at least we have some context.

Detlef: what kind of locks might be being used?

Pablo: condition variables are 'our' locks so this is less of a problem. condition_variable_any might be more problematic.

The general direction is to call terminate if the lock cannot be reacquired.

Pablo: Can we change the wording to 'leaves the mutex locked' ?

Hans: so if the unlock throws we simply propagate the exception.

Move the issue to open and add some formal wording at a later time.

[2013-09 Chicago: Resolved]

Detlef improves wording. Daniel suggests to introduce a Remarks element for the special "If the function fails to meet the postcondition..." wording and applies this to the proposed wording.

Proposed resolution:

This wording is relative to N3691.

  1. Edit 33.5.3 [thread.condition.condvar] as indicated:

    void wait(unique_lock<mutex>& lock);
    

    […]

    -10- Effects:

    • Atomically calls lock.unlock() and blocks on *this.

    • When unblocked, calls lock.lock() (possibly blocking on the lock), then returns.

    • The function will unblock when signaled by a call to notify_one() or a call to notify_all(), or spuriously.

    • If the function exits via an exception, lock.lock() shall be called prior to exiting the function scope.

    -?- Remarks: If the function fails to meet the postcondition, std::terminate() shall be called (18.5.1 [except.terminate]). [Note: This can happen if the re-locking of the mutex throws an exception. — end note]

    -11- Postcondition: lock.owns_lock() is true and lock.mutex() is locked by the calling thread.

    -12- Throws: Nothingsystem_error when an exception is required (30.2.2).

    -13- Error conditions:

    • equivalent error condition from lock.lock() or lock.unlock().

    template <class Predicate>
    void wait(unique_lock<mutex>& lock, Predicate pred);
    

    […]

    -?- Remarks: If the function fails to meet the postcondition, std::terminate() shall be called (18.5.1 [except.terminate]). [Note: This can happen if the re-locking of the mutex throws an exception. — end note]

    -16- Postcondition: lock.owns_lock() is true and lock.mutex() is locked by the calling thread.

    -17- Throws: system_error when an exception is required (30.2.2), timeout-related exceptions (30.2.4), or any exception thrown by pred.

    -18- Error conditions:

    • equivalent error condition from lock.lock() or lock.unlock().

    template <class Clock, class Duration>
      cv_status wait_until(unique_lock<mutex>& lock,
        const chrono::time_point<Clock, Duration>& abs_time);
    

    […]

    -20- Effects:

    • […]

    • If the function exits via an exception, lock.lock() shall be called prior to exiting the function scope.

    -?- Remarks: If the function fails to meet the postcondition, std::terminate() shall be called (18.5.1 [except.terminate]). [Note: This can happen if the re-locking of the mutex throws an exception. — end note]

    -21- Postcondition: lock.owns_lock() is true and lock.mutex() is locked by the calling thread.

    […]

    -23- Throws: system_error when an exception is required (30.2.2) or timeout-related exceptions (30.2.4).

    -24- Error conditions:

    • equivalent error condition from lock.lock() or lock.unlock().

    template <class Rep, class Period>
      cv_status wait_for(unique_lock<mutex>& lock,
        const chrono::duration<Rep, Period>& rel_time);
    

    […]

    -?- Remarks: If the function fails to meet the postcondition, std::terminate() shall be called (18.5.1 [except.terminate]). [Note: This can happen if the re-locking of the mutex throws an exception. — end note]

    -28- Postcondition: lock.owns_lock() is true and lock.mutex() is locked by the calling thread.

    […]

    -29- Throws: system_error when an exception is required (30.2.2) or timeout-related exceptions (30.2.4).

    -30- Error conditions:

    • equivalent error condition from lock.lock() or lock.unlock().

    template <class Clock, class Duration, class Predicate>
      bool wait_until(unique_lock<mutex>& lock,
        const chrono::time_point<Clock, Duration>& abs_time,
    	Predicate pred);
    

    […]

    -?- Remarks: If the function fails to meet the postcondition, std::terminate() shall be called (18.5.1 [except.terminate]). [Note: This can happen if the re-locking of the mutex throws an exception. — end note]

    -33- Postcondition: lock.owns_lock() is true and lock.mutex() is locked by the calling thread.

    […]

    -35- Throws: system_error when an exception is required (30.2.2), timeout-related exceptions (30.2.4), or any exception thrown by pred.

    -36- Error conditions:

    • equivalent error condition from lock.lock() or lock.unlock().

    template <class Rep, class Period, class Predicate>
      bool wait_for(unique_lock<mutex>& lock,
        const chrono::duration<Rep, Period>& rel_time,
    	Predicate pred);
    

    […]

    -?- Remarks: If the function fails to meet the postcondition, std::terminate() shall be called (18.5.1 [except.terminate]). [Note: This can happen if the re-locking of the mutex throws an exception. — end note]

    -40- Postcondition: lock.owns_lock() is true and lock.mutex() is locked by the calling thread.

    […]

    -42- Throws: system_error when an exception is required (30.2.2), timeout-related exceptions (30.2.4), or any exception thrown by pred.

    -43- Error conditions:

    • equivalent error condition from lock.lock() or lock.unlock().

  2. Edit 33.5.4 [thread.condition.condvarany] as indicated:

    template<class Lock>
    void wait(Lock& lock);
    

    […]

    -10- Effects:

    • Atomically calls lock.unlock() and blocks on *this.

    • When unblocked, calls lock.lock() (possibly blocking on the lock) and returns.

    • The function will unblock when signaled by a call to notify_one(), a call to notify_all(), or spuriously.

    • If the function exits via an exception, lock.lock() shall be called prior to exiting the function scope.

    -?- Remarks: If the function fails to meet the postcondition, std::terminate() shall be called (18.5.1 [except.terminate]). [Note: This can happen if the re-locking of the mutex throws an exception. — end note]

    -11- Postcondition: lock is locked by the calling thread.

    -12- Throws: Nothingsystem_error when an exception is required (30.2.2).

    -13- Error conditions:

    • equivalent error condition from lock.lock() or lock.unlock().

    template <class Lock, class Clock, class Duration>
      cv_status wait_until(Lock& lock, const chrono::time_point<Clock, Duration>& abs_time);
    

    […]

    -15- Effects:

    • […]

    • If the function exits via an exception, lock.lock() shall be called prior to exiting the function scope.

    -?- Remarks: If the function fails to meet the postcondition, std::terminate() shall be called (18.5.1 [except.terminate]). [Note: This can happen if the re-locking of the mutex throws an exception. — end note]

    -16- Postcondition: lock is locked by the calling thread.

    […]

    -18- Throws: system_error when an exception is required (30.2.2) or timeout-related exceptions (30.2.4).

    -19- Error conditions:

    • equivalent error condition from lock.lock() or lock.unlock().

    template <class Lock, class Rep, class Period>
      cv_status wait_for(Lock& lock, const chrono::duration<Rep, Period>& rel_time);
    

    […]

    -?- Remarks: If the function fails to meet the postcondition, std::terminate() shall be called (18.5.1 [except.terminate]). [Note: This can happen if the re-locking of the mutex throws an exception. — end note]

    -22- Postcondition: lock is locked by the calling thread.

    […]

    -23- Throws: system_error when an exception is required (30.2.2) or timeout-related exceptions (30.2.4).

    -24- Error conditions:

    • equivalent error condition from lock.lock() or lock.unlock().