This here is the follow-up to this question. I was recommended to implement a Lockable type (similar to std::mutex
) that can work with std::lock_guard
, std::scoped_lock
, etc. instead of unnecessarily writing a class similar to std::lock_guard
(thus violating D.R.Y). The result is a class named spinlock_mutex
. The below example snippet is very similar to what I'm trying to do in my multithreaded program.
The new MRE (live):
#include <atomic>
#include <mutex>
#include <thread>
#include <stop_token>
#include <vector>
#include <array>
#include <exception>
#include <fmt/core.h>
namespace util
{
class [[ nodiscard ]] spinlock_mutex
{
public:
using lockable_type = std::atomic_flag;
[[ nodiscard ]] constexpr
spinlock_mutex( ) noexcept = default;
spinlock_mutex( const spinlock_mutex& ) noexcept = delete;
spinlock_mutex& operator=( const spinlock_mutex& ) noexcept = delete;
void lock( ) noexcept
{
while ( m_lockable.test_and_set( std::memory_order_acquire ) )
{
while ( m_lockable.test( std::memory_order_relaxed ) ) { }
}
}
bool try_lock( ) noexcept
{
return m_lockable.test_and_set( std::memory_order_acquire );
}
void unlock( ) noexcept
{
m_lockable.clear( std::memory_order_release );
}
private:
lockable_type m_lockable { };
};
}
constinit util::spinlock_mutex mtx { };
constinit std::vector<std::exception_ptr> caught_exceptions { };
void func( const std::stop_source stop_src, const int base_value ) noexcept
{
for ( auto counter { 0uz }; !stop_src.stop_requested( ) && counter < 3; ++counter )
{
try
{
fmt::print( "base value: {} --- {}\n",
base_value, counter + static_cast<decltype( counter )>( base_value ) );
}
catch ( ... )
{
stop_src.request_stop( );
const std::lock_guard lock { mtx };
caught_exceptions.emplace_back( std::current_exception( ) );
}
}
}
int main( )
{
{ // a scope to limit the lifetime of jthread objects
std::stop_source stop_src { std::nostopstate };
std::array<std::jthread, 8> threads { };
try
{
stop_src = std::stop_source { };
for ( auto value { 0 }; auto& thrd : threads )
{
thrd = std::jthread { func, stop_src, value };
value += 5;
}
}
catch ( const std::exception& ex )
{
stop_src.request_stop( );
fmt::print( "{}\n", ex.what( ) );
}
}
for ( const auto& ex_ptr : caught_exceptions )
{
if ( ex_ptr != nullptr )
{
try
{
std::rethrow_exception( ex_ptr );
}
catch ( const std::exception& ex )
{
fmt::print( "{}\n", ex.what( ) );
}
}
}
}
I may have to mention that my aim is to inform the threads to return whenever an exception is caught in one thread. This is done using the copies of std::stop_source
that are passed to each thread.
So the constinit util::spinlock_mutex mtx { };
will only need to be locked pretty rarely since exceptions are thrown rarely. Is this a valid case to use a spinlock?
With that said, do the above program and its flow control make any sense? I have thought about what could go wrong (e.g. deadlocks, uncaught exceptions, etc.) but nothing worrying caught my attention.
The last for loop is there to handle the exceptions after all the std::jthread
s have been destructed. That's also why the jthread
s are declared in an inner scope.