relatively_random
relatively_random

Reputation: 5126

Simplest way to make std::thread exception safe

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?

Upvotes: 4

Views: 2310

Answers (1)

DaoWen
DaoWen

Reputation: 33019

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:

Upvotes: 6

Related Questions