• Home
  • /
  • Blog
  • /
  • An Improved Thread with C++20

February 12, 2021

std::jthread stands for joining thread. In addition to std::thread (C++11), std::jthread automatically joins in its destructor and can cooperatively be interrupted. Read this post to know why std::jthread should be your first choice.

 TimelineCpp20

The following table gives you a concise overview of the functionality of std::jthread.

jthread

 

For additional details, please refer to cppreference.com. When you want to read more posts about std::thread, here are they: my post about std::thread.

First, why do we need an improved thread in C++20? Here is the first reason.

Automatically Joining

This is the non-intuitive behavior of std::thread. If a std::thread is still joinable, std::terminate is called in its destructor. A thread thr is joinable if neither thr.join() nor thr.detach() was called. Let me show what that means.

// threadJoinable.cpp

#include <iostream>
#include <thread>

int main() {
    
    std::cout << '\n';
    std::cout << std::boolalpha;
    
    std::thread thr{[]{ std::cout << "Joinable std::thread" << '\n'; }};
    
    std::cout << "thr.joinable(): " << thr.joinable() << '\n';
    
    std::cout << '\n';
    
}

 

When executed, the program terminates when the local object thr goes out of scope.

threadJoinable

Both executions of std::thread terminate. In the second run, the thread thr has enough time to display its message: Joinable std::thread.

In the next example, I use std::jthread from the C++20 standard.

// jthreadJoinable.cpp

#include <iostream>
#include <thread>

int main() {
    
    std::cout << '\n';
    std::cout << std::boolalpha;
    
    std::jthread thr{[]{ std::cout << "Joinable std::thread" << '\n'; }};
    
    std::cout << "thr.joinable(): " << thr.joinable() << '\n';
    
    std::cout << '\n';
    
}

 

Now, the thread thr automatically joins in its destructor if it’s still joinable such as in this case.

jthreadJoinable

But this is not all that std::jthread  provides additionally to std::thread. A std::jthread can be cooperatively interrupted. I already presented the general ideas of cooperative interruption in my last post: Cooperative Interruption of a Thread in C++20.

Cooperative Interruption of a std::jthread

To get a general idea, let me present a simple example.

// interruptJthread.cpp

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

using namespace::std::literals;

int main() {
    
    std::cout << '\n';
    
    std::jthread nonInterruptable([]{                           // (1)
        int counter{0};
        while (counter < 10){
            std::this_thread::sleep_for(0.2s);
            std::cerr << "nonInterruptable: " << counter << '\n'; 
            ++counter;
        }
    });
    
    std::jthread interruptable([](std::stop_token stoken){     // (2)
        int counter{0};
        while (counter < 10){
            std::this_thread::sleep_for(0.2s);
            if (stoken.stop_requested()) return;               // (3)
            std::cerr << "interruptable: " << counter << '\n'; 
            ++counter;
        }
    });
    
    std::this_thread::sleep_for(1s);
    
    std::cerr << '\n';
    std::cerr << "Main thread interrupts both jthreads" << '\n';
    nonInterruptable.request_stop();
    interruptable.request_stop();                              // (4)
    
    std::cout << '\n';
    
}

 

In the main program, I start the two threads nonInterruptable and interruptable (lines 1)and 2). Unlike in the thread nonInterruptable , the thread interruptable gets a std::stop_token and uses it in line (3) to check if it was interrupted: stoken.stop_requested(). In case of a stop request, the lambda function returns, and the thread ends. The call interruptable.request_stop() (line 4) triggers the stop request. This does not hold for the previous call nonInterruptable.request_stop() . The call has no effect.

interruptJthread

To complete my post, with C++20, you can also cooperatively interrupt a condition variable.

New wait Overloads for std::condition_variable_any

Before I write about std::condition_variable_any, here are my post about condition variables

The three wait variations wait, wait_for, and wait_until of the std::condition_variable_any get new overloads. These overloads take a std::stop_token.

template <class Predicate>
bool wait(Lock& lock,  
          stop_token stoken,
          Predicate pred);

template <class Rep, class Period, class Predicate>
bool wait_for(Lock& lock, 
              stop_token stoken, 
              const chrono::duration<Rep, Period>& rel_time, 
              Predicate pred);
                
template <class Clock, class Duration, class Predicate>
bool wait_until(Lock& lock, 
                stop_token stoken,
                const chrono::time_point<Clock, Duration>& abs_time, 
                Predicate pred);

 

These new overloads need a predicate. The presented versions ensure getting notified if a stop request for the passed std::stop_token stoken is signaled. They return a boolean that indicates whether the predicate evaluates to true. This returned boolean is independent of whether a stop was requested or whether the timeout was triggered.

After the wait calls, you can check if a stop request occurred.

cv.wait(lock, stoken, predicate);
if (stoken.stop_requested()){
    // interrupt occurred
}

 

The following example shows the usage of a condition variable with a stop request.

// conditionVariableAny.cpp

#include <condition_variable>
#include <thread>
#include <iostream>
#include <chrono>
#include <mutex>
#include <thread>

using namespace std::literals;

std::mutex mutex_;
std::condition_variable_any condVar;

bool dataReady;

void receiver(std::stop_token stopToken) {                 // (1)

    std::cout << "Waiting" << '\n';

    std::unique_lock<std::mutex> lck(mutex_);
    bool ret = condVar.wait(lck, stopToken, []{return dataReady;});
    if (ret){
        std::cout << "Notification received: " << '\n';
    }
    else{
         std::cout << "Stop request received" << '\n';
    }
}

void sender() {                                            // (2)

    std::this_thread::sleep_for(5ms);
    {
        std::lock_guard<std::mutex> lck(mutex_);
        dataReady = true;
        std::cout << "Send notification"  << '\n';
    }
    condVar.notify_one();                                  // (3)

}

int main(){

  std::cout << '\n';

  std::jthread t1(receiver);
  std::jthread t2(sender);
  
  t1.request_stop();                                       // (4)

  t1.join();
  t2.join();

  std::cout << '\n';
  
}

 

The receiver thread (line 1) is waiting for the notification of the sender thread (line 2). Before the sender thread sends its notification (line 3), the main thread triggers a stop request in line (4). The program’s output shows that the stop request happened before the notification.

conditionVariableAny

What’s next?

What happens when your write without synchronization to std::cout? You get a mess. Thanks to C++20, we have synchronized output streams.

 

 

Leave a Reply


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

{"email":"Email address invalid","url":"Website address invalid","required":"Required field missing"}

Related Posts