std::thread
class is inherently exception-unsafe since its destructor calls std::terminate
.
std::thread t( function );
// do some work
// (might throw!)
t.join();
You could, of course, put everything in between construction and join()
in a try-catch block, but this can get tedious and error-prone if you know you want to join or detach no matter what happens.
So I was thinking how would one go about writing the simplest possible wrappers around it, but that would also support other hypothetical types of threads. For instance, boost::thread
or something completely different, as long as it had joinable()
, join()
and detach()
methods. Here's how far I've got:
// handles threads safely
// Acts the same as the underlying thread type, except during destruction.
// If joinable, will call join (and block!) during destruction.
// Keep in mind that any exception handling will get delayed because of that;
// it needs to wait for the thread to finish its work first.
template <class UNDERLYING_THREAD = std::thread>
class scoped_thread: public UNDERLYING_THREAD
{
public:
typedef UNDERLYING_THREAD thread_type;
using thread_type::thread_type;
scoped_thread()
: thread_type() {}
scoped_thread( scoped_thread && other )
: thread_type( std::move( other ) ) {}
scoped_thread & operator = ( scoped_thread && other )
{
thread_type & ref = *this;
ref = std::move( other );
return *this;
}
~scoped_thread()
{
if( thread_type::joinable() )
thread_type::join();
}
};
// handles autonomous threads safely
// Acts the same as the underlying thread type, except during destruction.
// If joinable, will call detach during destruction.
// Make sure it doesn't use any scoped resources since the thread can remain
// running after they go out of scope!
template <class UNDERLYING_THREAD = std::thread>
class free_thread
{
// same except it calls detach();
}
This seems to work, but I'm wondering if there is a way to avoid manually defining the constructors and the move assignment operator. Probably the biggest issue I noticed is that compilation will fail if you supply a class with deleted move constructor as a template argument.
Do you have any suggestions about how to possibly avoid this? Or are there other, bigger issues with this approach?
If you want proper exception handling with asynchronous tasks, maybe you should use std::future
rather than std::thread
. Instead of using join()
, you'd use get()
on the future, and if the future threw an exception, then get()
will result in the same exception.
A simple example:
#include <future>
#include <iostream>
int my_future_task(int my_arg) {
throw std::runtime_error("BAD STUFF!");
return my_arg;
}
int main(int argc, char* argv[]) {
auto my_future = std::async(my_future_task, 42);
try {
my_future.get();
}
catch(std::exception &e) {
std::cout << "Caught exception: " << e.what() << std::endl;
}
return 0;
}
See also:
std::future::get
std::future_error
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With