Andy
Andy

Reputation: 3829

Benefits of using std::stop_source and std::stop_token instead of std::atomic<bool> for deferred cancellation?

When I run several std::threads in parallell and need to cancel other threads in a deferred manner if one thread fails I use a std::atomic<bool> flag:

#include <thread>
#include <chrono>
#include <iostream>

void threadFunction(unsigned int id, std::atomic<bool>& terminated) {
    srand(id);
    while (!terminated) {
        int r = rand() % 100;
        if (r == 0) {
            std::cerr << "Thread " << id << ": an error occured.\n";
            terminated = true; // without this line we have to wait for other thread to finish
            return;
        }
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }
}

int main()
{
    std::atomic<bool> terminated = false;
    std::thread t1(&threadFunction, 1, std::ref(terminated));
    std::thread t2(&threadFunction, 2, std::ref(terminated));

    t1.join();
    t2.join();
    std::cerr << "Both threads finished.\n";
    int k;
    std::cin >> k;
}

However now I am reading about std::stop_sourceand std::stop_token. I find that I can achieve the same as above by passing both a std::stop_sourceby reference and std::stop_token by value to the thread function? How would that be superior?

I understand that when using std::jthread the std::stop_token is very convenient if I want to stop threads from outside the threads. I could then call std::jthread::request_stop() from the main program.

However in the case where I want to stop threads from a thread is it still better?

I managed to achieve the same thing as in my code using std::stop_source:

 void threadFunction(std::stop_token stoken, unsigned int id, std::stop_source source) {
    srand(id);
    while (!stoken.stop_requested()) {
        int r = rand() % 100;
        if (r == 0) {
            std::cerr << "Thread " << id << ": an error occured.\n";
            source.request_stop(); // without this line we have to wait for other thread to finish
            return;
        }
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }
}
int main()
{
    std::stop_source source;
    std::stop_token stoken = source.get_token();
    std::thread t1(&threadFunction, stoken, 1, source);
    std::thread t2(&threadFunction, stoken, 2, source);
    t1.join();
    t2.join();
    std::cerr << "Both threads finished.\n";
    int k;
    std::cin >> k;
}

Using std::jthread would have resulted in more compact code:

std::jthread t1(&threadFunction, 1, source);
std::jthread t2(&threadFunction, 2, source);

But that did not seem to work.

Upvotes: 7

Views: 4733

Answers (2)

Lorah Attkins
Lorah Attkins

Reputation: 5856

Apart from being more expressive and communicating intentions better, stop_token and friends achieve something really important for jthread. To understand it you have to consider its destructor which looks something like this:

~jthread() 
{
    if(joinable()) 
    {
        // Not only user code, but the destructor as well
        // will let your callback know it's time to go.
        request_stop(); 
        
        join();
    }
}

by encapsulating a stop_source, jthread facilitates what is called cooperative cancellation. As you've also noted, you never have to pass the stop_token to a jthread, just provide a callback that accepts the token as its first parameter. What happens next is that the class can detect that your callback accepts a stop token and pass a token to its internal stop source when calling it.

What does this mean for cooperative cancellation? Safer termination of course! Since jthread will always attempt to join on destruction, it now has the means to prevent endless loops and deadlocks where two or more threads wait for each other to finish. By using stop_token your code can make sure that it can safely join when it's time to go.

However in the case where I want to stop threads from a thread is it still better?

Now regarding the feature you are requesting, that's what C# calls "linked cancellation". Yes, there are requests and discussions to add a parameter in the jthread constructor so that it can refer to an external stop source, but that's not yet available (and has many implications). Doing something similar purely with stop tokens would require a stop_callback to tie all cancellations together, but still it could be suboptimal (as shown in the link). The bottom line is that jthread needs stop_token, but in some cases you may not need jthread, especially if the following solution does not appeal to you:

stop_source ssource;
std::stop_callback cb {ssource.get_token(), [&] { 
   t1.request_stop();
   t2.request_stop(); 
}};

ssource.request_stop(); // This stops boths threads. 

The good news is that if you don't fall into the suboptimal pattern described in the link (i.e. you don't need an asynchronous termination), then this functionality is easy to abstract into a utility, something like:

auto linked_cancellations = [](auto&... jthreads) {
  stop_source s;
  return std::make_pair(s, std::stop_callback{
    s.get_token(), [&]{ (jthreads.request_stop(), ...); }}); 
};

which you'd use as

auto [stop_source, cb] = linked_cancellations(t1, t2); 
// or as many thread objects as you want to link ^^^

stop_source.request_stop(); // Stops all the threads that you linked.

Now if you want to control the linked threads from within the thread, I'd use the initial pattern (std::atomic<bool>), since having a callback with both a stop token and a stop source is somewhat confusing.

Upvotes: 4

Nicol Bolas
Nicol Bolas

Reputation: 473966

It didn't work because std::jthread has a special feature where, if the first parameter of a thread-function is a std::stop_token, it fills that token in by an internal stop_source object.

What you ought to do is only pass a stop_source (by value, not by reference), and extract the token from it within your thread function.

As for why this is better than a reference to an atomic, there are a myriad of reasons. The first being that stop_source is a lot safer than a bare reference to an object whose lifetime is not under the local control of the thread function. The second being that you don't have to do std::ref gymnastics to pass parameters. This can be a source of bugs since you might accidentally forget to do that in some place.

The standard stop_token mechanism has features beyond just requesting and responding to a stop. Since the response to a stop happens at an arbitrary time after issuing it, it may be necessary to execute some code when the stop is actually requested rather than when it is responded to. The stop_callback mechanism allows you to register a callback with a stop_token. This callback will be called in the thread of the stop_source::request_stop call (unless you register the callback after the stop was requested, in which case it's called right when you register it). This can be useful in limited cases, and it's not simple code to write yourself. Especially when all you have is an atomic<bool>.

And then there's simple readability. Passing a stop_source tells you exactly what is going on without having to even see the name of a parameter. Passing an atomic<bool> tells you very little from just the typename; you have to look at the parameter name or its usage in the function to know that it is for halting the thread.

Upvotes: 6

Related Questions