...

/

Additional Synchronization Primitives in C++20

Additional Synchronization Primitives in C++20

Learn about the new synchronization primitives in C++20 (latches and barriers) and how they're used.

C++20 comes with a few additional synchronization primitives, namely std::latch, std::barrier, and std::counting_semaphore (and the template specialization std::binary_semaphore). This lesson will be an overview of these new types and some typical scenarios where they can be useful. We’ll begin with std::latch.

Using latches

A latch is a synchronization primitive that can be used for synchronizing multiple threads. It creates a synchronization point where all threads must arrive at. We can think of a latch as a decrementing counter. Typically, all threads decrement the counter once and then wait for the latch to reach zero before moving on.

A latch is constructed by passing an initial value of the internal counter:

Press + to interact
auto lat = std::latch{8}; // Construct a latch initialized with 8

Threads can then decrement the counter using count_down():

Press + to interact
lat.count_down(); // Decrement but don't wait

A thread can wait on the latch to reach zero:

Press + to interact
lat.wait(); // Block until zero

It’s also possible to check (without blocking) to see whether the counter has reached zero:

Press + to interact
if (lat.try_wait()) {
// All threads have arrived ...
}

It’s common to wait for the latch to reach zero right after decrementing the counter, as follows:

Press + to interact
lat.count_down();
lat.wait();

In fact, this use case is common enough to deserve a tailor-made member function; arrive_and_wait() decrements the latch and then waits for the latch to reach zero:

Press + to interact
lat.arrive_and_wait(); // Decrement and block while not zero

Joining a set of forked tasks is a common scenario when working with concurrency. If the tasks only need to be joined at the end, we can use an array of future objects (to wait on) or just wait for all the threads to complete. But in other cases, we want a set of asynchronous tasks to arrive at a common synchronization point, and then have the tasks continue running. These situations typically occur when some initialization ...