C++ Latch and Barrier

Introduction

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.

wait_notify.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
#include <chrono>
#include <condition_variable>
#include <iostream>
#include <mutex>
#include <string>
#include <thread>

std::mutex m;
std::condition_variable cv;
std::string data;
bool ready = false;
bool processed = false;

void worker_thread()
{
std::cout << "Worker thread start" << std::endl;
std::unique_lock<std::mutex> lk(m);
cv.wait(lk, [] { return ready; });

// 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();
}

void master_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;
}

int main()
{
std::thread worker(worker_thread), master(master_thread);
// Workflow:
// master thread -> worker thread -> master thread.
worker.join();
master.join();
}
1
2
3
4
5
6
7
8
$ 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.

latch.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#include <chrono>
#include <iostream>
#include <latch>
#include <string>
#include <thread>

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

void worker_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
}

void master_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;
}

int main()
{
std::thread worker(worker_thread), master(master_thread);
// Workflow:
// master thread -> worker thread -> master thread.
worker.join();
master.join();
}
1
2
3
4
5
6
7
8
$ 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.

barrier.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#include <barrier>
#include <chrono>
#include <iostream>
#include <string>
#include <thread>

std::string data;
std::barrier sync_point(2); // Barrier for synchronization between master and worker threads

void worker_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
}

void master_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;
}

int main()
{
std::thread worker(worker_thread), master(master_thread);
// Workflow:
// master thread -> worker thread -> master thread.
worker.join();
master.join();
}
1
2
3
4
5
6
7
8
$ 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.

References

Author

Lei Mao

Posted on

02-06-2026

Updated on

02-06-2026

Licensed under


Comments