Concurrent programming is a fundamental necessity in the modern programming world, especially as projects and applications become more complex. Developers need to put more effort on handling concurrent programming safely.

C++20 introduced a new thread type, std::jthread, which has some additional features compared to std::thread, including a new destructor, new member functions, and new capabilities for managing threads. std::jthread is simply a wrapper class of std::thread, and its most significant features are automatic joining and cooperative interruption.


Automatic Joining


The destructor of a std::thread object with an associated thread calls std::terminate if join() has not been called. However, there are some cases where the std::thread object does not have an associated thread, such as if join() or detach() has been called, or if the std::thread object was default-constructed or moved. This means that developers must carefully call join() or detach() before destroying a std::thread object to avoid program termination.

To address this issue, C++20 introduced a new thread type, std::jthread, which automatically calls join() on destruction. This makes std::jthread a more convenient and safer choice for most concurrent programming tasks

The following two examples demonstrate the difference between the destructors of std::jthread and std::thread.

Destruction of std::thread without join() or detach() calls:

Code:

#include <iostream>
#include <thread>

void print_hello()
{
    std::cout << "Hello" << std::endl;
}

int main()
{
    std::thread normal_thread(print_hello);

    std::this_thread::sleep_for(std::chrono::milliseconds(100));

    return 0;
}

Output:

Hello
terminate called without an active exception
Aborted

Destruction of std::jthread without join() or detach() calls:

Code:

#include <iostream>
#include <thread>

void print_hello()
{
    std::cout << "Hello" << std::endl;
}

int main()
{
    std::jthread j_thread(print_hello);

    std::this_thread::sleep_for(std::chrono::milliseconds(100));

    return 0;
}

Output:

Hello

Cooperative Interruption

Every std::jthread has its associated stop-state, to determine if the thread should stop or not. std::stop_source and std::stop_token allow developers to control the stop-state of a std::jthread, but there are some differences between these two:
— std::stop_source encapsulates the associated stop-state in order to control and view it. It can control the lifetime of the stop-state via its request_stop() call. This call atomically changes the associated stop-state.
— std::stop_token provides a thread-safe view to the related stop-state. It cannot change the stop-state directly, like std::stop_source can, but via it’s stop_requested() and stop_possible() calls it can check whether a stop request sent or sending a stop request is possible.

In general, std::stop_source should be used when developers need to explicitly control the stop-state of a std::jthread, while std::stop_token should be used when developers only need to check the state of the stop-state.

#include <iostream>
#include <thread>

int main ()
{
    // [1]
    std::stop_source s_source;
    std::stop_token s_token;

    // [2]
    std::cout << "Stop possible: " << s_token.stop_possible() << std::endl;
    std::cout << "Stop requested: " << s_token.stop_requested() << std::endl;

    // [3]
    auto my_thread = std::jthread(
        [](std::stop_token stop_token)
        {
            int count = 0;
            while (!stop_token.stop_requested())
            {
                std::cout << count++ << std::endl;
                std::this_thread::sleep_for(std::chrono::milliseconds(250));
            }
        }
    );

    // [4]
    s_source = my_thread.get_stop_source();
    s_token = my_thread.get_stop_token();

    std::this_thread::sleep_for(std::chrono::seconds(2));

    // [5]
    if (s_source.stop_possible())
    {
        s_source.request_stop();
        std::cout << "Stop request sent" << std::endl;
    }

    // [6]
    std::cout << "Stop possible: " << s_source.stop_possible() << std::endl;
    std::cout << "Stop requested: " << s_source.stop_requested() << std::endl;

    return 0;
}

Output:

Stop possible: 0
Stop requested: 0
0
1
2
3
4
5
6
7
Stop request sent
Stop possible: 1
Stop requested: 1

— [1]: Created a std::stop_source object and a std::stop_tokenobject.
— [2]: Checked whether a stop is possible or requested by the std::stop_token ( std::stop_source may also be used in this part). At this point, since they have not been associated with a stop-state yet, neither of this outputs will be true.
— [3]: Created a std::jthread object with a lambda function that takes a std::stop_token object as a parameter. The function will execute until the stop_token.stop_requested() call returns true.
— [4]: Got the associated std::stop_source and std::stop_token objects from my_thread and assigned them to s_source and s_token. After that, waited for 2 seconds to let my_thread execute for a while.
— [5]: After checking if sending a stop request is possible or not, sent a stop request via s_sourcestd::jthread will continue to execute until its current cycle finishes. After that, it will check the std::stop_token object again and will exit from the loop.
— [6]: This part is a little bit tricky. As you guessed, stop_requested() call returns true because a stop request was just sent. The confusing part is that the stop_possible() call also returns true in this state. It is because stop_possible() call is just checking if the object has been associated with a stop-state, so its return value is independent from whether any stop request is sent or not.


In this post, a simple introduction to std::jthread has been made. Although these new features of std::jthread do not make std::thread deprecated, knowing the features of both of them ensures that we can prefer the more eligible one for different scenarios.

Leave a Reply

Your email address will not be published. Required fields are marked *