Reputation: 2907
Let's say I have a class like this that can copy a value without throwing:
#include <type_traits>
template<typename T>
requires std::is_nothrow_copy_constructible_v<T>
&& std::is_nothrow_copy_assignable_v<T>
class ValueCopier
{
public:
// Constructor
ValueCopier(T const& object) noexcept
: _object{object}
{
// Do nothing
}
// Return a copy of a stored object
T getCopy() const noexcept
{
return _object;
}
// Change the stored object
void setValue(T const& object) noexcept
{
_object = object;
}
private:
T _object;
};
Now the question is: is it possible to make such a class thread-safe while keeping the functions noexcept
?
I can make the class thread-safe using std::atomic
:
#include <atomic>
#include <type_traits>
template<typename T>
requires std::is_nothrow_copy_constructible_v<T>
&& std::is_nothrow_copy_assignable_v<T>
class ValueCopier
{
public:
// Constructor
ValueCopier(T const& object) noexcept
: _object{object}
{
// Do nothing
}
// Return a copy of a stored object
T getCopy() const noexcept
{
return _object.load();
}
// Change the stored object
void setValue(T const& object) noexcept
{
_object.store(object);
}
private:
std::atomic<T> _object;
};
but then I won't be able to use the class with some types since std::atomic<T>
can only be used with trivially copyable types.
I can also make the class thread-safe with std::mutex
:
#include <mutex>
#include <type_traits>
template<typename T>
requires std::is_nothrow_copy_constructible_v<T>
&& std::is_nothrow_copy_assignable_v<T>
class ValueCopier
{
public:
// Constructor
ValueCopier(T const& object) noexcept
: _object{object}
{
// Do nothing
}
// Return a copy of a stored object
T getCopy() const // Not noexcept
{
auto const lock = std::lock_guard<std::mutex>(_mutex);
return _object;
}
// Change the stored object
void setValue(T const& object) // Not noexcept
{
auto const lock = std::lock_guard<std::mutex>(_mutex);
_object = object;
}
private:
mutable std::mutex _mutex;
T _object;
};
but then the functions are no longer noexcept
because locking the mutex can throw.
Am I missing something? Are there maybe preconditions that when verified guarantee that locking a mutex cannot throw? Or some type other than std::mutex
that would make it possible to write this?
Technically I guess a spinlock on an std::atomic<bool>
would do but it seems wasteful
Upvotes: 2
Views: 79
Reputation: 1930
I think you need to make a distinction between std::mutex::lock()
not being marked noexcept
because it technically can fail, and whether it will actually fail in your case.
Let's first discuss in which conditions std::mutex::lock()
and std::mutex::unlock()
can actually fail.
On Linux GCC's libstdc++6 calls pthread_mutex_lock()
and pthread_mutex_unlock()
and throws an exception if these fail. First of all, let's check what types of mutexes glibc supports:
std::mutex
uses PTHREAD_MUTEX_INITIALIZER
to initialize the mutex, which defaults to a timed mutex.
With this in mind, glibc on Linux the pthread_mutex_lock()
and pthread_mutex_unlock()
can fail in the following circumstances:
pthread_mutex_lock()
failures on LinuxEAGAIN
if the mutex is a recursive mutex and the lock count (unsigned int
in glibc source, so 32bit on Linux) has wrapped around. Since std::mutex
is not a recursive mutex, this will not happen.EDEADLK
if the mutex is an error-checking mutex and a deadlock is detectd. Since std::mutex
is not an error-checking mutex, this will not happen.EOWNERDEAD
if the mutex was locked by a thread that no longer exists. Unless you force-exit the current thread in the assingment operator of the wrapped type T
(but just the thread, without killing the program), this will never happen with your example implementation class.ENOTRECOVERABLE
if the mutex is considered 'unrecoverable' (which can only occur once pthread_mutex_unlock()
has returned ENOTRECOVERABLE
for the same mutex at least once). Since std::mutex
is not a recursive mutex, this will not happen.EINVAL
if the mutex was not initialized correctly. Since you use std::mutex
this will not happen.EINVAL
if the mutex was created with PTHREAD_PRIO_PROTECT
and the current thread's priority is higher than the specified priority ceiling. Since std::mutex
does not do that this will not happen.(See the source code for pthread_mutex_lock for details.)
pthread_mutex_unlock()
failures on LinuxEPERM
if you try to unlock a recursive mutex of which you are not the current owner. Since std::mutex
is not a recursive, this will not happen.ENOTRECOVERABLE
if a recursive mutex was locked by a thread that has since died while holding the lock and pthread_mutex_unlock()
is called by another thread. Since std::mutex
is not a recursive, this will not happen.EINVAL
if the mutex was not initialized correctly. Since you use std::mutex
this will not happen.(See the source code for pthread_mutex_unlock for details.)
Apple's clang/libc++ implementation also calls pthread_mutex_lock()
and pthread_mutex_unlock()
on macOS. Let's look at the manual to see when these can fail:
pthread_mutex_lock()
failures on macOSEINVAL
if the mutex was not properly initialized. Since you use std::mutex
this will not happen.EDEADLK
if a deadlock would otherwise occur. Deadlocks with mutexes can only occur if the order in which multiple mutexes are locked is not the same. Unless the copy assignment operator / copy constructor of your wrapped type T
locks another global mutex (which it should never do), this will not happen.pthread_mutex_unlock()
failures on macOSEINVAL
if the mutex was not properly initialized. Since you use std::mutex
this will not happen.EPERM
if the mutex was not locked by the current thread. Since your use of std::mutex
only unlocks it when the same thread locked it, this will not happen.Microsoft's STL uses its own implementation of std::mutex
, which uses WinAPI's AcquireSRWLockExclusive()
and ReleaseSRWLockExclusive()
with some additional checks.
(The source code to #include mutex and mutex.cpp are of interest here.)
Let's see in what way Microsoft's STL can fail for locking/unlocking:
std::mutex::lock()
failures on Microsoft's STL_Throw_Cpp_error(_RESOURCE_DEADLOCK_WOULD_OCCUR)
if mtx_do_lock()
doesn't succeed; but for a std::mutex
that can only fail if you try to lock the same mutex multiple times (and the mutex is not recursive). Your codes never does that, so this will not happen._Throw_Cpp_error(_RESOURCE_UNAVAILABLE_TRY_AGAIN)
if you attempt to lock a recursive mutex and the lock count has wrapped around. Since std::mutex
is not recursive, this will not happen.std::mutex::unlock()
failures on Microsoft's STLMicrosoft's STL marks std::mutex::unlock()
as noexcept
to indicate it can never fail, and the code is written indeed in such a manner that that's true.
MinGW on Windows has two different threading implementations that you can choose, winpthreads and mcfgthread. With winpthreads GCC's libstdc++6 will use pthread_mutex_lock()
and pthread_mutex_unlock()
, while mcfgthread tries to call Windows kernel functions directly.
Let's look at the various failure modes:
pthread_mutex_lock()
failures with winpthreadsENOMEM
; winpthreads allocates mutexes on the stack, and if it can't allocate memory on the stack when the mutex is locked for the first time, this will fail. This is actually a case when std::mutex::lock()
could genuinely fail for you if the system is completely out of memory. (Or at least your process is out of virtual memory if e.g. on 32bit.)EDEADLK
if you attempt to lock a non-recursive mutex you already hold the lock to. This will not happen in your case.EPERM
; winpthreads uses a WinApi Event object internally for notifications, if it cannot create that due to permissions, this will fail. This is actually a case when std::mutex::lock()
could genuinely fail for you.ENOMEM
; if winpthreads cannot create the WinApi Event object because the system is out of resources (e.g. cannot create the event handle). This is actually a case when std::mutex::lock()
could genuinely fail for you.EINVAL
if the mutex was not properly initialized. This will not happen in your case.(See the mutex.cpp of winpthreads for the source code.)
pthread_mutex_unlock()
failures with winpthreadsENOMEM
in the case of error-checking mutexes. This will not happen in your case.EINVAL
if pthread_mutex_unlock()
is called on an unlocked mutex. This will not happen in your case.EPERM
if you try to unlock a mutex that is not owned by the current thread. This will not happen in your case.EPERM
if WinApi's SetEvent()
fails due to permission errors. This is actually a case when std::mutex::unlock()
could genuinely fail for you._MCF_mutex_lock()
failures with mcfgthread(See mcfgthread's mutex.c source code for details. Only timed mutexes might return -1
.)
_MCF_mutex_unlock()
failures with mcfgthreadAfter having looked at all of the major implementations of std::mutex
, the way your second example using std::mutex
is written is such that you can easily mark the getCopy()
and setValue()
methods noexcept
without any downsides, because they will in fact not actually throw in practice for your usage of std::mutex
and std::lock_guard
.
The only exception is MinGW/GCC with winpthreads as the backend, because that apparently implemented its mutex with heap allocations and event objects that require the kernel to allocate handles, which obviously can fail.
That said, you could always use a custom mutex implementation such as this one that uses SRWLock
on Windows (regardless of MSVC/MinGW) and pthread_mutex
on all other systems:
#if defined(_WIN32)
#include <windows.h>
#else
#include <pthread.h>
#include <errno.h>
#include <string.h>
#endif
#include <exception>
#include <system_error>
#include <iostream>
class custom_mutex
{
public:
#if defined(_WIN32)
using native_handle_type = PSRWLOCK;
#else
using native_handle_type = pthread_mutex_t*;
#endif
constexpr inline custom_mutex() noexcept
{
}
inline ~custom_mutex() noexcept
{
#if defined(_WIN32)
// Do nothing, there is no DeleteSRWLock function
#else
int r = pthread_mutex_destroy(&m_mutex);
if (r == EBUSY) {
/* We are trying to destroy a locked mutex, this means the
* program is in an invalid state.
*/
std::terminate();
}
#endif
}
custom_mutex(custom_mutex const&) = delete;
custom_mutex& operator=(custom_mutex const&) = delete;
inline void lock()
{
#if defined(_WIN32)
AcquireSRWLockExclusive(&m_srwLock);
#else
int r = pthread_mutex_lock(&m_mutex);
if (r != 0)
throw std::system_error(r, std::system_category(), strerror(r));
#endif
}
inline bool try_lock()
{
#if defined(_WIN32)
return TryAcquireSRWLockExclusive(&m_srwLock);
#else
int r = pthread_mutex_trylock(&m_mutex);
if (r != 0) {
if (r == EBUSY)
return false;
throw std::system_error(r, std::system_category(), strerror(r));
}
return true;
#endif
}
void unlock()
{
#if defined(_WIN32)
ReleaseSRWLockExclusive(&m_srwLock);
#else
int r = pthread_mutex_unlock(&m_mutex);
if (r != 0)
throw std::system_error(r, std::system_category(), strerror(r));
#endif
}
inline native_handle_type native_handle()
{
#if defined(_WIN32)
return &m_srwLock;
#else
return &m_mutex;
#endif
}
private:
#if defined(_WIN32)
SRWLOCK m_srwLock = SRWLOCK_INIT;
#else
pthread_mutex_t m_mutex = PTHREAD_MUTEX_INITIALIZER;
#endif
};
While this implementation of a mutex can technically still throw in lock()
and unlock()
, it will not do that in your specific usage of that class, because of the discussion how pthread_mutex_lock()
and pthread_mutex_unlock()
actually react. Also, in the Windows case it doesn't do any error checking at all, so technically doesn't fulfill the contract of std::mutex
(which is why Microsoft's STL has the deadlock check in lock()
in addition to the call to AcquireSRWLockExclusive()
).
Upvotes: 3