0

In general it is a good practice to declare a swap and move noexcept as that allows to provide some exception guarantee. At the same time writing a thread-safe class often implies adding a mutex protecting the internal resources from races. If I want to implement a swap function for such a class the straightforward solution is to lock in a safe way the resources of both arguments of the swap and then perform the resource swap as, for example, clearly answered in the answer to this question: Implementing swap for class with std::mutex .

The problem with such an algorithm is that a mutex lock is not noexcept, therefore swap cannot, strictly speaking, be noexcept. Is there a solution to safely swap two objects of a class with a mutex?

The only possibility that comes to my mind is to store the resource as a handle so that the swap becomes a simple pointer swap which can be done atomically. Otherwise one could consider the lock exceptions as unrecoverable error which should anyway terminate the program, but this solution feels like just a way to put the dust under the carpet.

EDIT: As came out in the comments, I know that the exceptions thrown by the mutexes are not arbitrary but then the question can be rephrased as such:

Are there robust practices to limit the situation a mutex can throw to those when it is actually an unrecoverable OS problem? What comes to my mind is to check, in the swap algorithm, whether the two objects to swap are not the same. That is a clear deadlock situation which will trigger an exception in the best case scenario but can be easily checked for. Are there other similar triggers which one can safely check to make a swap function robust and practically noexcept for all the situation that matter?

Triskeldeian
  • 590
  • 3
  • 18
  • 1
    Doesn't seem meaningful to me to lock inside a move, because as soon as the move ends threads might start working with the moved value before you reassign it. So in this case you would lock both move and assignment explicitly anyway – Alexey S. Larionov Jun 07 '20 at 20:35
  • @AlexLarionov Sure, it would be unsafe to move a shared object but this is a user problem. I'm not sure that non-locking while moving would make the situation any better. And anyway the problem with swap stays – Triskeldeian Jun 07 '20 at 21:36
  • as the last resort you can catch all the exceptions when you lock. though the execution time might become unpredictable – Alexey S. Larionov Jun 07 '20 at 21:42
  • yes, but that means either skipping the swap or doing it on an unlocked resource which may leave it in an undefined state. I think I'd rather terminate than silently create such a potential mess – Triskeldeian Jun 07 '20 at 21:46
  • @Triskeldeian The fact that `std::mutex::lock` is `noexcept(false)` doesn't mean it is okay for it throw arbitrary exceptions. If you are using it correctly - it should never throw (unless you are running on Windows XP and earlier but the whole thing was broken anyway). –  Jun 08 '20 at 08:13
  • @StaceyGirl your point is well made, they are not arbitrary, but how to use those mutexes so that you avoid them to throw exceptions, beside those case when, due to system issues they are anyway unrecoverable? – Triskeldeian Jun 08 '20 at 08:50
  • 1
    @Triskeldeian I think with `std::mutex` most implementations try to return errors when locking already locked mutex or using uninitialized mutex. Those are UB anyway. I think the point of `lock` being `noexcept(false)` is to signal about errors in program, not to handle them. Think of `~unique_lock`: it is `noexcept(true)`, but calls `unlock` which is `noexcept(false)`. If something fails, the whole program get terminated - and that is how most mutexes are used. –  Jun 08 '20 at 09:08
  • Thanks @StaceyGirl BTW I think you should write that as a proper answer – Triskeldeian Jun 08 '20 at 09:11
  • IME I've never written a thread-safe class with an internal mutex which was _also_ a value object I might want to swap or move. Inter-thread structures (queues, task pools, schedulers or whatever) have identity, so generally don't get moved. Are you sure you actually need this? – Useless Jun 08 '20 at 09:32
  • @Useless I don't need to swap the mutex but I need to lock the mutex to be able to swap the shared resource, in this case a cointainer. – Triskeldeian Jun 08 '20 at 10:10

1 Answers1

2

On POSIX systems it is common for std::mutex to be a thin wrapper around pthread_mutex_t, for which lock and unlock function can fail when:

  • There is an attempt to acquire already owned lock
  • The mutex object is not initialized or has been destroyed already

Both of the above are UB in C++ and are not even guaranteed to be returned by POSIX. On Windows both are UB if std::mutex is a wrapper around SRWLOCK.

So it seems that the main point of allowing lock and unlock functions to throw is to signal about errors in program, not to make programmer expect and handle them.

This is confirmed by the recommended locking pattern: the destructor ~unique_lock is noexcept(true), but is supposed to call unlock which is noexcept(false). That means if exception is thrown by unlock function, the whole program gets terminated by std::terminate.

The standard also mentions this:

The error conditions for error codes, if any, reported by member functions of the mutex types shall be:

(4.1) — resource_unavailable_try_again — if any native handle type manipulated is not available.

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

(4.3) — invalid_argument — if any native handle type manipulated as part of mutex construction is incorrect

In theory you might encounter operation_not_permitted error, but situations when this happens are not really defined in the standard.

So unless you cause UB in your program related to the std::mutex usage or use the mutex in some OS-specific scenario, quality implementations of lock and unlock should never throw.

Among the common implementations, there is at least one that might be of low quality: std::mutex implemented on top of CRITICAL_SECTION in old versions of Windows (I think Windows XP and earlier) can throw after failing to lazily allocate internal event during contention. On the other hand, even earlier versions allocated this event during initialization to prevent failing later, so std::mutex::mutex constructor might need to throw there (even though it is noexcept(true) in the standard).