Search⌘ K
AI Features

Concurrency

Explore the concurrency features introduced in C++20, including atomic operations with std::atomic_ref, atomic smart pointers, semaphores, latches, barriers, cooperative thread interruption with std::jthread, and synchronized output streams. Understand how these tools improve thread safety and synchronization in concurrent programming.

Atomics

The class template std::atomic_ref applies atomic operations to the referenced non-atomic object. Concurrent writing and reading of the referenced object can take place, therefore, with no data race. The lifetime of the referenced object must exceed the lifetime of the std::atomic_ref. Accessing a sub-object of the referenced object with std::atomic_ref is not thread-safe.

According to std::atomic, std::atomic_ref can be specialized and supports specializations for the built-in data types.

struct Counter { 
  int a;
  int b; 
};
Counter counter;
std::atomic_ref<Counter> cnt(counter);

With C++20, we get two atomic smart pointers that are partial specializations of std::atomic: std::atomic<std::shared_ptr<T>> and std::atomic<std::weak_ptr<T>>. Both atomic smart pointers guarantee that not only the control block, as in the case of std::shared_ptr, is thread-safe but also the associated object.

std::atomic gets more extensions (click here for more details). C++20 provides specializations for atomic floating-point types. This is quite convenient when you have a concurrently incremented floating-point type. A value of type std::atomic_flag is a kind of atomic boolean. It has a cleared and set state. For simplicity reasons, I call the clear state false and the set state true. The clear() member function enables you to set its value to false. With the test_and_set() member function, you can set the value to true and get the previous value. There is no member function to ask for the current value. This will change with C++20 because std::atomic_flag has a test() method.

Furthermore, std::atomic_flag can be used for thread synchronization via the member functions notify_one(), notify_all(), and wait(). With C++20, notifying and waiting are available on all partial and full specializations of std::atomic and std::atomic_ref. Specializations are available for bools, integrals, floats, and pointers.

Semaphores

Semaphores are a synchronization mechanism used to control concurrent access to a shared resource. A counting semaphore, such as the one which was added in C++20, is a special semaphore whose initial counter is bigger than zero. The counter is initialized in the constructor. Acquiring the semaphore decreases the counter, and releasing the semaphore increases the counter. If a thread tries to acquire the semaphore when the counter is zero, the thread blocks until another thread increments the counter by releasing the semaphore.

Latches and barriers

Latches and barriers are straightforward thread synchronization mechanisms that enable some threads to block until a counter becomes zero.

  • You can use a std::latch only once.

  • A std::latch is useful for managing one task by multiple threads.

  • You can use a std::barrier more than once.

  • A std::barrier is useful for managing repeated tasks by multiple threads.

Furthermore, std::barrier can adjust the counter in each iteration.

The following is based on a code snippet from proposal N4204. I fixed a few typos and reformatted it.

C++ 17
void DoWork(threadpool* pool) {
std::latch completion_latch(NTASKS);
for (int i = 0; i < NTASKS; ++i) {
pool->add_task([&] {
// perform work
...
completion_latch.count_down();
});
}
// Block until work is done
completion_latch.wait();
}

The counter of the std::latch completion_latch is set to NTASKS (line 2). The thread pool executes NTASKS jobs (lines 4 - 10). At the end of each job, the counter is decremented (line 8). The thread running function DoWork blocks in line 12 until all tasks have been finished.

Cooperative interruption

Thanks to std::stop_token(), a std::jthread can be interrupted cooperatively.

C++ 17
int main() {
std::cout << '\n';
std::jthread nonInterruptable([] {
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) {
int counter{0};
while (counter < 10){
std::this_thread::sleep_for(0.2s);
if (stoken.stop_requested()) return;
std::cerr << "interruptable: " << counter << '\n';
++counter;
}
});
std::this_thread::sleep_for(1s);
std::cerr << '\n';
std::cerr << "Main thread interrupts both jthreads" << std:: endl;
nonInterruptable.request_stop();
interruptable.request_stop();
std::cout << '\n';
}

This is the output of the program:

The main program starts two threads: nonInterruptable and interruptable (line 4 and line 13 respectively). Only the interruptable thread gets a std::stop_token, which it uses in line 17 to check if it is interrupted. The lambda immediately returns in case of an interruption.

The call to interruptable.request_stop() triggers the cancellation of the thread. Calling nonInterruptable.request_stop() has no effect.

std::jthread

std::jthread stands for “joining thread”. std::jthread extends std::thread by automatically joining the started thread. std::jthread can also be interrupted.

std::jthread is added to the C++20 standard because of 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.

C++ 17
#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';
}

The output of the program on the first attempt is:

The output of the program on the second attempt is:

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

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

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

Now, the thread thr automatically joins in its destructor if necessary.

Synchronized outputstreams

With C++20, we get synchronized outputstreams. What happens when more threads write concurrently to std::cout without synchronization?

C++ 17
void sayHello(std::string name) {
std::cout << "Hello from " << name << '\n';
}
int main() {
std::cout << "\n";
std::jthread t1(sayHello, "t1");
std::jthread t2(sayHello, "t2");
std::jthread t3(sayHello, "t3");
std::jthread t4(sayHello, "t4");
std::jthread t5(sayHello, "t5");
std::jthread t6(sayHello, "t6");
std::jthread t7(sayHello, "t7");
std::jthread t8(sayHello, "t8");
std::jthread t9(sayHello, "t9");
std::jthread t10(sayHello, "t10");
std::cout << '\n';
}

You may get a mess as follows:

Switching from std::cout in the function sayHello to std::osyncstream(std::cout) turns the mess into harmony.

C++ 17
void sayHello(std::string name) {
td::osyncstream(std::cout) << "Hello from " << name << '\n';
}

This time we get a synchronized output: