New threads: C++20 std::jthread

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 (via get_stop_source, get_stop_token and request_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.