Exception handling for <mutex> and <condition_variable>
Asked Answered
L

2

11

Assuming

  1. no undefined behaviour occurs,
  2. no deadlocks occur,
  3. mutexes are locked and unlocked in the correct order by the correct threads the correct number of times,
  4. non-recursive mutexes are not locked multiple times,
  5. locking recursive mutexes does not exceed the maximum level of ownership,
  6. no predicates passed to condition variables throw, and
  7. only clocks, time points, and durations provided by the standard library are used with the std:: mutexes and condition variables

is it guaranteed that operating on the different types of std:: mutexes and condition variables (other than on constructing them) does not throw any exceptions (especially of type std::system_error)?

For example, in case of methods like:

void MyClass::setVariable() {
    std::lock_guard<std::mutex> const guard(m_mutex);
    m_var = 42; // m_var is of type int
    m_conditionVariable.notify_all();
}

void MyClass::waitVariable() {
    std::unique_lock<std::mutex> lock(m_mutex);
    m_conditionVariable.wait(lock, [this]() noexcept { return m_var == 42; });
}

Is it safe to assume noexcept or should one write some try-catch blocks around the callsites? Or are there any caveats?

Please consider all types of mutexes and condition variables in C++11, C++14 and later.

Longer answered 13/5, 2016 at 8:51 Comment(3)
you could trace through the futex() implementation on linux to look for conditions where it fails: github.com/torvalds/linux/blob/master/kernel/futex.c#L3147Coblenz
std::condition_variable::wait() is changed to noexcept in C++14. It now just calls std::terminate() when reacquiring the lock fails. You might want to consider that.Cucullate
@Cucullate I don't agree. Can you quote some documentation?Incommensurable
S
2

Thank's to the link T.C. provided now I'd say yes — your code should be safe. Since in the future standard device_or_resource_busy will be removed and as the author of the issue says that this situation can't occur in any reasonable way then there are only 2 possibilities for lock to throw:

(13.1) — operation_not_permitted — if the thread does not have the privilege to perform the operation.

(13.2) — resource_deadlock_would_occur — if the implementation detects that a deadlock would occur.

And both of these situations are excluded by your preconditions. So your code should be safe to use noexcept.

Spitter answered 13/5, 2016 at 9:20 Comment(2)
Can you provide any real-life examples of why or where a device_or_resource_busy might be thrown during the operations under discussion?Longer
@jotik, I can't but it doesn't mean that it is not possible.Spitter
I
9

Short answer: No (sorry)

Any of these operations will throw std::system_error if the underlying synchronisation object fails to perform its operation.

This is because correct operation of synchronisation primitives depends on:

  1. available system resources.

  2. some other part of the program not invalidating the primitive

Although in fairness, if (1) is happening it's probably time to redesign the application or run it on a less-loaded machine.

And if (2) is happening, the program is not logically consistent.

That being said,

or should one write some try-catch blocks around the callsites?

Also no.

You should write try/catch blocks under the following conditions:

  1. Where the program is in a position to do something useful about the error condition (such as repairing it or asking the user if he wants to try again)

  2. You would like to add some information to the error and re-throw it in order to provide a diagnostic breadcrumb trail (nested exceptions, for example)

  3. You wish to log the failure and carry on.

Otherwise, the whole point of c++ exception handling is that you allow RAII to take care of resource reacquisition and allow the exception to flow up the call stack until is finds a handler that wants to handle it.

example of creating a breadcrumb trail:

void wait_for_object()
try
{
    _x.wait();  // let's say it throws a system_error on a loaded system
}
catch(...)
{
  std::throw_with_nested(std::runtime_error(__func__));
}
Incommensurable answered 13/5, 2016 at 9:12 Comment(9)
I think (2) was already considered by my assumptions 1 and 3. But can you give a real-life example of why (1) might happen, i.e. system resources run out?Longer
@Longer I can see where you're going with this thinking. Essentially you're asking "are there any circumstances under which I can ignore the return codes of system synchronisation functions"? In reality almost everyone does almost all the time, because the circumstances under which they will fail are (very) rare in practice. Nevertheless, the standard says that these functions may throw an exception, and library implementors may therefore throw exceptions for reasons that are best understood by them. Therefore we must assume that an exception is possible.Incommensurable
@jotikThat's not to say that every exception needs to be handled. If it's very rare, and will only happen on a loaded system (in which other parts of the program may well be failing), it's almost always reasonable to catch the exception in main (or thread_main), report it and abort.Incommensurable
Maybe it were better to use noexcept and let std::terminate() or std::unexpected() report these instead of allowing these to propagate to main()? This might lose some context, but would allow to write more efficient code.Longer
@Longer the actual performance cost of including exception handling is minimal when the non-exceptional code path is taken. The time cost of using a synchronisation primitive is orders of magnitude more. It really isn't worth worrying about.Incommensurable
@Longer @Richard Hodges Like I noted in the other comment, as of C++14 std::condition_variable::wait() is noexcept and calls std::terminate() on any problem. I image the rationale behind this is that those error conditions happen rarely and are difficult to recover from: You will need some kind of inter-thread communication to pass the exception while the exception being that inter-thread communication is failing.Cucullate
@Cucullate are you sure about that? It's not what I see in the documentation. std::condition_variable::notify() is noexcept, but not wait()Incommensurable
@RichardHodges I got it from here en.cppreference.com/w/cpp/thread/condition_variable/wait Of course, I don't know if that's correct.Cucullate
@Cucullate just checked the latest standard draft § 30.5.1 here open-std.org/JTC1/SC22/WG21/docs/papers/2015/n4527.pdf You're right in the case of wait() with no predicate. wait(cv, pred) can throw if the predicate throws.Incommensurable
S
2

Thank's to the link T.C. provided now I'd say yes — your code should be safe. Since in the future standard device_or_resource_busy will be removed and as the author of the issue says that this situation can't occur in any reasonable way then there are only 2 possibilities for lock to throw:

(13.1) — operation_not_permitted — if the thread does not have the privilege to perform the operation.

(13.2) — resource_deadlock_would_occur — if the implementation detects that a deadlock would occur.

And both of these situations are excluded by your preconditions. So your code should be safe to use noexcept.

Spitter answered 13/5, 2016 at 9:20 Comment(2)
Can you provide any real-life examples of why or where a device_or_resource_busy might be thrown during the operations under discussion?Longer
@jotik, I can't but it doesn't mean that it is not possible.Spitter

© 2022 - 2024 — McMap. All rights reserved.