New threads: C++20 std::jthread
Article directory
-
- 1. What is std::jthread?
- 2. Why introduce jthread?
- 3. How to use
-
- 3.1 Automatic join()
- 3.2 Thread interruption
- 4. Summary
1. What is std::jthread
Class jthread
represents a single thread of execution. It has the usual behavior of std::thread
, except that jthread
automatically recombines on destruction and can be canceled/stopped under specific circumstances.
2. Why introduce jthread
std::jthread
Based on std::thread
, it adds a new feature that can actively cancel or stop thread execution. Compared to std::thread
, std::jthread
has an exception-safe thread termination process and can be replaced in most cases with little or no effort Change the code. Before we get into the details, let’s talk about the flaws of std::thread
:
When using std::jthread
, you need to wait for the thread to end through join()
, continue the execution of the statement after join()
, or call detach()
to detach the thread from the current thread and move it to the background to continue running.
A std::thread instance can be in either the joinable or unjoinable state. A std::thread that is default constructed, detached, or moved is unjoinable. We must join a joinable std::thread explicitly before the end of its life; otherwise, the std::thread’s destructor calls std::terminate, whose default behavior is to abort the process.
A std::thread instance can be in a joinable or unjoinable state. A default-constructed, detached, or moved std::thread is not joinable. We must explicitly join the connectable std::thread before its lifetime ends; otherwise, the std::thread’s destructor will call std::terminate, whose default behavior is to abort the process.
void FuncWithoutJoinOrDetach() { std::thread t{task, task_args}; // No t.join() or t.detach() was called } // std::terminate() will be called when the life cycle of t ends, and the program will end abnormally
As shown in the above code, if t.join()
or t.detach()
is not called, when the thread object t
life cycle ends Sometimes, a core dump may be generated, causing the program to terminate abnormally.
In the above example, after instantiating the object t
, even if the join()
function of thread t
is called, sometimes it may take a long time to wait. Only then can the task
of thread t
be executed, or even wait forever (for example, there is an infinite loop in task
), because thread
does not allow us to actively kill it like a process, so when an infinite loop occurs in t
, it will result in the inability to continue executing the statements after jion()
. The started thread can only end its own execution or end the entire program to end the thread.
Based on the above two main reasons, the std::jthread
class was introduced in C++20 to make up for the shortcomings of std::tread
. In addition to having std In addition to the behavior of ::thread
, the following two new functions are mainly added:
-
When the
std::jthread
object is destructed,join
will be automatically called to wait for the execution flow it represents to end. -
std::jthread
supports external request abort.
3. How to use
The basic usage of std::jthred
is the same as that of std::thread
. We will not go into details here. Below we will focus on the two new features it adds through several examples. function.
3.1 Automatic join()
Let’s first look at an error example of std::thread
:
#include <iostream> #include <thread> int main() {<!-- --> std::cout << '\ '; std::cout << std::boolalpha; std::thread thr{<!-- -->[]{<!-- --> std::cout << "Joinable std::thread" << '\ '; }}; std::cout << "thr.joinable(): " <<thr.joinable() << '\ '; std::cout << '\ '; return 0; }
Possible output:
thr.joinable(): true libc++abi: terminating [1] 2326 abort ./test
You can see from the output that the program terminated abnormally.
Below we will replace thread
with jthread
, because the object thr
of jthread
will be automatically called when it is destructed. Its own join
function ensures that the main thread waits for thr
to complete before proceeding to the next step.
#include <iostream> #include <thread> int main() {<!-- --> std::cout << '\ '; std::cout << std::boolalpha; std::jthread thr{<!-- -->[] {<!-- --> std::cout << "Joinable std::thread" << '\ '; }}; std::cout << "thr.joinable(): " <<thr.joinable() << '\ '; std::cout << '\ '; return 0; }
Output:
thr.joinable(): true Joinable std::thread
3.2 Thread interruption
For thread interruption, std::jthread
mainly introduces the following three stop signal processing:
- get_stop_source() : Returns the
stop_source
object associated with the thread’s stop status. - get_stop_token() : Returns the
stop_token
associated with the thread’s shared stop status. - **request_stop() **: Request execution to stop via the thread’s shared stop state.
Let’s briefly understand its specific usage through a few examples.
Example 1:
#include <chrono> #include <iostream> #include <thread> using namespace::std::literals; int main() {<!-- --> std::cout << '\ '; std::jthread nonInterruptable([]{<!-- --> // Step (1) int counter{<!-- -->0}; while (counter < 10){<!-- --> std::this_thread::sleep_for(0.2s); std::cerr << "nonInterruptable: " << counter << '\ '; + + counter; } }); std::jthread interruptable([](std::stop_token token){<!-- --> // Step (2) int counter{<!-- -->0}; while (counter < 10){<!-- --> std::this_thread::sleep_for(0.2s); if (stoken.stop_requested()) return; // Step (3) std::cerr << "interruptable: " << counter << '\ '; + + counter; } }); std::this_thread::sleep_for(1s); std::cerr << '\ '; std::cerr << "Main thread interrupts both jthreads" << '\ '; nonInterruptable.request_stop(); // Step (4) interruptable.request_stop(); // Step (5) std::cout << '\ '; }
Output:
nonInterruptable: interruptable: 00 nonInterruptable: 1 interruptable: 1 nonInterruptable: 2 interruptable: 2 nonInterruptable: 3 interruptable: 3 Main thread interrupts both jthreads nonInterruptable: 4 nonInterruptable: 5 nonInterruptable: 6 nonInterruptable: 7 nonInterruptable: 8 nonInterruptable: 9
In Example 1, we start two threads: nonInterruptable
(step 1) and ninterruptable
(step 2). Unlike nonInterruptable
, interruptable
gets a std::stop_token
and uses it in step 3 to check if it was interrupted (i.e. stoken.stop_requested()
). If a stop request occurs, the lambda function will return and the interruptable
thread will end. The main program calls interruptable.request_stop();
in step 5, triggering a stop request. At this time, the interruptable
thread stops and no longer prints. Although nonInterruptable.request_stop();
is called in step 4, the request is not processed in nonInterruptable
, so nonInterruptable
continues to execute. , until the main program ends.
Example 2:
#include <chrono> #include <iostream> #include <thread> using namespace std::chrono_literals; int main() {<!-- --> std::cout << std::boolalpha; auto print = [](std::string_view name, const std::stop_source & amp;source) {<!-- --> std::cout << name << ": stop_possible = " << source.stop_possible(); std::cout << ", stop_requested = " << source.stop_requested() << '\ '; }; //A worker thread auto worker = std::jthread([](std::stop_token token) {<!-- --> for (int i = 10; i; --i) {<!-- --> std::this_thread::sleep_for(300ms); if (stoken.stop_requested()) {<!-- --> std::cout << " Sleepy worker is requested to stop\ "; return; } std::cout << " Sleepy worker goes back to sleep\ "; } }); std::stop_source stop_source = worker.get_stop_source(); print("stop_source", stop_source); std::cout << "\ Pass source to other thread:\ "; auto stopper = std::thread( [](std::stop_source source) {<!-- --> std::this_thread::sleep_for(500ms); std::cout << "Request stop for worker via source\ "; source.request_stop(); }, stop_source); stopper.join(); std::this_thread::sleep_for(200ms); std::cout << '\ '; print("stop_source", stop_source); }
Output:
stop_source: stop_possible = true, stop_requested = false Pass source to other thread: Sleepy worker goes back to sleep Request stop for worker via source Sleepy worker is requested to stop stop_source: stop_possible = true, stop_requested = true
Example 3:
#include <iostream> #include <thread> #include <chrono> using namespace std; int main() {<!-- --> auto f = [](const stop_token & amp;st) {<!-- --> // jthread is responsible for passing in stop_token while (!st.stop_requested()) {<!-- --> // jthread will not force the thread to stop, we need to cancel/stop the operation based on the status of stop_token cout << "other: " <<this_thread::get_id() << "\ "; this_thread::sleep_for(1s); } cout << "other thread stopped!\ "; }; jthread jth(f); cout << "main: " <<this_thread::get_id() << "\ "; this_thread::sleep_for(5s); jth.request_stop(); // Request to stop the thread, the stop_requested() function of the corresponding stop_token returns true (note that in addition to manual calling, this function will also be automatically called when jthread is destroyed) // We don't need to call join() on jthread, it automatically joins when destroyed } }
Possible output:
main: other: 140166751352704 140166751336192 other: 140166751336192 other: 140166751336192 other: 140166751336192 other: 140166751336192 other thread stopped!
4. Summary
jthread
is based on std::thread
and mainly adds the following two functions:
- When the
jthread
object is destructed,join
will be automatically called to wait for the execution flow it represents to end. jthread
supports external request abort (viaget_stop_source
,get_stop_token
andrequest_stop
).
The automatic join
and external request abort features in std::jthread
make it easier to write safer code, but its performance is relative to thread
code> also adds overhead.
The article’s first official account: iDoitnow. If you like it, you can follow it.