Scheduling threads and synchronizing their execution is a common requirement in concurrent programming. In C++11, the std::condition_variable was introduced to facilitate thread synchronization. However, creating such mechanisms can be complex and error-prone. In C++20, two new synchronization primitives, std::latch and std::barrier, were introduced to simplify thread synchronization in certain scenarios.
In this blog post, I would like to demonstrate how to use create new implementations using std::latch and std::barrier to replace an existing producer-consumer model example implementation using std::condition_variable.
C++ Condition Variable
In my previous blog post “C++ Condition Variable”, I demonstrated how to use std::condition_variable introduced in C++11 to synchronize threads using the following producer-consumer model example.
$ g++ wait_notify.cpp -o wait_notify -std=c++11 $ ./wait_notify Worker thread start Master thread start Master thread signals data ready for processing Worker thread is processing data Worker thread signals data processing completed Back in master thread, data = Example data after processing
C++ Latch and Barrier
The above example works fine, but it requires careful management of mutexes, condition variables, and spurious wakeups, which is somewhat tedious and confusing. The same producer-consumer model example can be implemented more straightforwardly using the new synchronization primitives std::latch or std::barrier introduced in C++20.
The key difference between std::latch and std::barrier is that a latch is a one-time-use synchronization primitive, while a barrier can be reused multiple times. A latch is typically used when you want to wait for a set of threads to complete their tasks before proceeding, whereas a barrier is used when you want to synchronize a set of threads at multiple points in their execution.
C++ Latch
Compared to the implementation using std::condition_variable, the implementation using std::latch is much simpler and easier to understand.
$ g++ latch.cpp -o latch -std=c++20 $ ./latch Worker thread start Master thread start Master thread signals data ready for processing Worker thread is processing data Worker thread signals data processing completed Back in master thread, data = Example data after processing
C++ Barrier
Similarly, the same example could be implemented using std::barrier. Note that in this case, instead of using two latches, we can reuse a single barrier for synchronization between the master and worker threads.
$ g++ barrier.cpp -o barrier -std=c++20 $ ./barrier Worker thread start Master thread start Master thread signals data ready for processing Worker thread is processing data Worker thread signals data processing completed Back in master thread, data = Example data after processing
Conclusions
The std::latch and std::barrier synchronization primitives introduced in C++20 are important concepts for thread synchronization especially used in producer-consumer models. They provide a simpler and more intuitive way to manage thread synchronization compared to traditional methods using mutexes and condition variables.