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.
// After the wait, we own the lock. std::cout << "Worker thread is processing data" << std::endl; data += " after processing";
// Send data back to master thread processed = true; std::cout << "Worker thread signals data processing completed" << std::endl;
// Manual unlocking is done before notifying, to avoid waking up // the waiting thread only to block again (see notify_one for details) lk.unlock(); // The worker thread has done the work, // Notify the master thread to continue the work. cv.notify_one(); }
voidmaster_thread() { std::cout << "Master thread start" << std::endl; data = "Example data"; // Send data to the worker thread. { std::lock_guard<std::mutex> lk(m); ready = true; std::cout << "Master thread signals data ready for processing" << std::endl; } // The master thread has done the preliminary work, // Notify the worker thread to continue the work. cv.notify_one();
// Wait for the worker. { std::unique_lock<std::mutex> lk(m); cv.wait(lk, [] { return processed; }); } std::cout << "Back in master thread, data = " << data << std::endl; }
$ 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.
std::string data; std::latch worker_latch(1); // Latch for worker thread to wait for master thread std::latch master_latch(1); // Latch for master thread to wait for worker thread
voidworker_thread() { std::cout << "Worker thread start" << std::endl; // Wait for the master thread to signal that data is ready worker_latch.wait();
// After the wait, we can process the data. std::cout << "Worker thread is processing data" << std::endl; data += " after processing";
// Signal the master thread that processing is completed std::cout << "Worker thread signals data processing completed" << std::endl; master_latch.count_down(); // Decrement the latch count to signal completion }
voidmaster_thread() { std::cout << "Master thread start" << std::endl; data = "Example data"; // Signal the worker thread that data is ready std::cout << "Master thread signals data ready for processing" << std::endl; worker_latch.count_down(); // Decrement the latch count to signal readiness
// Wait for the worker thread to complete processing master_latch.wait(); std::cout << "Back in master thread, data = " << data << std::endl; }
$ 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.
std::string data; std::barrier sync_point(2); // Barrier for synchronization between master and worker threads
voidworker_thread() { std::cout << "Worker thread start" << std::endl; // Wait for the master thread to signal that data is ready sync_point.arrive_and_wait();
// After the wait, we can process the data. std::cout << "Worker thread is processing data" << std::endl; data += " after processing";
// Signal the master thread that processing is completed std::cout << "Worker thread signals data processing completed" << std::endl; sync_point.arrive_and_wait(); // Wait for the master thread to reach the barrier }
voidmaster_thread() { std::cout << "Master thread start" << std::endl; data = "Example data"; // Signal the worker thread that data is ready std::cout << "Master thread signals data ready for processing" << std::endl; sync_point.arrive_and_wait(); // Wait for the worker thread to reach the barrier
// Wait for the worker thread to complete processing sync_point.arrive_and_wait(); std::cout << "Back in master thread, data = " << data << std::endl; }
$ 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.