C++20 Concurrency: <stop_token>

A simple way to handle cancellation of asynchronous tasks

CMP
3 min readNov 8, 2023

In a previous article, I talked about using std::jthread as a simpler and safer way to create multi-threaded C++ programs using C++20. However, I did not discuss the benefits of using the C++20 <stop_token> library header alongside std::jthread for simple cancellation of coroutines and asynchronous tasks.

What is the <stop_token> header used for?

This header contains a few useful components, including std::stop_token, std::stop_source, and std::stop_callback. Simply put, a std::stop_source represents a request to stop a std::jthread, while a std::stop_token allows you to query if a cancellation request has been made. If you wish to register a callback function to execute when a std::jthread is stopped, you can do so with std::stop_callback.

How to use std::stop_token ?

A std::stop_token object is not constructed directly; instead, it is retrieved from a std::stop_source, either directly or indirectly via a std::jthread, which holds an internal std::stop_source private member. Below is a simple example modified from the C++ reference demonstrating the usage with std::jthread:

#include <iostream>
#include <stop_token>
#include <thread>

using namespace std::literals::chrono_literals;

void printIncrementingValues(std::stop_token stopToken, int value) {
while (!stopToken.stop_requested()) {
std::cout << value++ << " " << std::flush;
std::this_thread::sleep_for(500ms);
}
std::cout << std::endl;
}

int main() {
// Print incrementing values starting with 1 for 5 seconds
std::jthread thread(printIncrementingValues, 1);
std::this_thread::sleep_for(5s);

// Destructor of std::jthread will call request_stop() and join(),
// but they can be explicitly called as well if needed for specific
// synchronization scenarios
thread.request_stop();
thread.join();

return 0;
}
Possible output:

1 2 3 4 5 6 7 8 9 10

In this example, a std::jthread was created to run the printIncrementingValues worker function with a single int parameter. However, notice that the worker function takes two parameters: a std::stop_token and an int! As strange as it may seem, you do not actually construct and pass in a std::stop_token, you simply add it as the first parameter argument to a function.

The worker function uses the stop_requested() method of the std::stop_token to check if a cancellation request has been made by the caller — who makes the request by calling the request_stop() method on the std::jthread.

How to use std::stop_source?

As mentioned previously, a std::jthread has an internal std::stop_source member for managing the stop state of the thread. However, it is possible to construct a separate std::stop_source that can be shared amongst multiple threads. Below is an example demonstrating this usage:

#include <iostream>
#include <stop_token>
#include <thread>
#include <vector>

using namespace std::literals::chrono_literals;

void printIncrementingValues(int value, std::stop_source stopSource) {
std::stop_token stopToken = stopSource.get_token();
while (!stopToken.stop_requested()) {
std::cout << value++ << " " << std::flush;
std::this_thread::sleep_for(500ms);
}
std::cout << std::endl;
}

int main() {
std::stop_source sharedStopSource;
std::vector<std::jthread> threads;

for (int i = 0; i < 3; i++) {
threads.emplace_back(std::jthread(printIncrementingValues, i, sharedStopSource));
}
std::this_thread::sleep_for(5s);

// Request stop for all threads using shared std::stop_source object
sharedStopSource.request_stop();

return 0;
}

Here, the printIncrementingValues worker function is modified to actually take in two parameters: an int and a std::stop_source, rather than indirectly taking a std::stop_token like the first example. Inside the function, the std::stop_token associated with the std::stop_source is retrieved using the get_token() method.

The caller creates 3 threads with each running the same worker function with the same shared std::stop_source object. When the caller requests a stop using the request_stop() method of std::stop_source, all worker routines are cancelled together.

How to use std::stop_callback?

If you’d like to execute a callback function after a std::jthread is stopped, you can create a std::stop_callback using the associated std::stop_token of the thread. Here is an example:

#include <iostream>
#include <stop_token>
#include <thread>

using namespace std::literals::chrono_literals;

void printIncrementingValues(std::stop_token stopToken, int value) {
while (!stopToken.stop_requested()) {
std::cout << value++ << " " << std::flush;
std::this_thread::sleep_for(500ms);
}
std::cout << std::endl;
}

int main() {
std::jthread thread(printIncrementingValues, 1);

// Get the std::stop_token associated with the std::jthread
std::stop_token stopToken = thread.get_stop_token();

// Register a callback
std::stop_callback callback(stopToken, []() {
std::cout << "Callback executed" << std::endl;
});

std::this_thread::sleep_for(5s);

return 0;
}
Possible output:

1 2 3 4 5 6 7 8 9 10
Callback executed

Here, the first example is modified to register a callback using the std::stop_token that is associated with the std::jthread.

Conclusion

The <stop_token> header introduces three important components for modern C++ concurrency: std::stop_token, std::stop_source, and std::stop_callback, which can be used in conjunction with std::jthread threads to manage cancellation of couroutines and asynchronous tasks. Cancellation requests are made through a std::stop_source, which contains a std::stop_token used to query if a request has been made and/or to register a std::stop_callback. Additionally, each std::jthread contains an internal std::stop_source, making it very easy to utilize the benefits of std::stop_token from within a worker function running in the std::jthread.

--

--

CMP

Software engineer specializing in operating systems, navigating the intracicies of the C++ language and systems programming.