Enjoy my training video on the C++ barrier...
Suppose three workers prepare data at different speeds, but a computation must begin only after all workers are ready. How do we synchronize them?
std::barrier is the answer in C++.
The std::barrier class is a synchronization primitive introduced in C++20. It allows a set of threads to synchronize at a certain point in their execution. It is similar to the std::latch class, but it can be reused multiple times.
A std::barrier object is initialized with a count, which specifies the number of threads that must reach the barrier before any of them can proceed. When a thread reaches the barrier, it calls the wait() method. If the count is not yet zero, the thread will be blocked until the count reaches zero. Once the count reaches zero, all of the threads that are waiting on the barrier will be released and can proceed.
The std::barrier class can be used to implement a variety of synchronization patterns, such as producer-consumer queues, parallel algorithms, and race condition prevention.
Here's my application in which I used barrier to showcase how it can be used.
Class Student
#include <iostream>
#include <string>
#include <thread>
#include <barrier>
#include <chrono>
#include <vector>
#include <syncstream> // For thread-safe console output
#include <format> // For C++20 string formatting
class Student {
private:
std::string name;
int timeLapseBeforeStarting; // in milliseconds
int timeToFinish; // in milliseconds
public:
// Optimized constructor using std::move to avoid redundant copies
Student(std::string studentName, int lapse, int finish)
: name(std::move(studentName)),
timeLapseBeforeStarting(lapse),
timeToFinish(finish) {}
// Virtual destructor is fine if subclassing, but defaulted here
virtual ~Student() = default;
void task(std::barrier<>& b) {
// 1. Simulate varying arrival times
std::this_thread::sleep_for(std::chrono::milliseconds(timeLapseBeforeStarting));
// 2. Synchronize at the barrier
b.arrive_and_wait();
auto now = std::chrono::system_clock::now();
// Locate the current system timezone and convert UTC to local time
auto local_time = std::chrono::current_zone()->to_local(now);
// std::osyncstream guarantees this block of output won't interleave with other threads
std::osyncstream(std::cout)
<< name << " is Starting the task at "
<< std::format("{:%F %T}", local_time) << "\n";
// 4. Simulate performing the task
std::this_thread::sleep_for(std::chrono::milliseconds(timeToFinish));
std::osyncstream(std::cout) << name << " finished the task.\n";
}
};
// ==========================================
// Class classETC
// ==========================================
#include "Student.h"
class classETC {
public:
Student ridit {"Ridit", 1000, 2000};
Student ishan {"Ishan", 3000, 1000};
Student rajdeep {"Rajdeep", 900, 1500};
classETC() = default;
virtual ~classETC() = default;
// FIX: Returns a container of jthreads to main() to keep them alive
auto giveTaskToStudent(std::barrier<>& b) {
std::vector<std::jthread> workers;
workers.reserve(3);
// Emplace threads directly into the vector
workers.emplace_back(&Student::task, &this->ridit, std::ref(b));
workers.emplace_back(&Student::task, &this->ishan, std::ref(b));
workers.emplace_back(&Student::task, &this->rajdeep, std::ref(b));
return workers; // Named Return Value Optimization (NRVO) handles this cleanly
}
};
#include <iostream>
#include <string>
#include <thread>
#include <barrier>
#include <chrono>
#include <vector>
#include <syncstream> // For thread-safe console output
#include <format> // For C++20 string formatting
class Student {
private:
std::string name;
int timeLapseBeforeStarting; // in milliseconds
int timeToFinish; // in milliseconds
public:
// Optimized constructor using std::move to avoid redundant copies
Student(std::string studentName, int lapse, int finish)
: name(std::move(studentName)),
timeLapseBeforeStarting(lapse),
timeToFinish(finish) {}
// Virtual destructor is fine if subclassing, but defaulted here
virtual ~Student() = default;
void task(std::barrier<>& b) {
// 1. Simulate varying arrival times
std::this_thread::sleep_for(std::chrono::milliseconds(timeLapseBeforeStarting));
// 2. Synchronize at the barrier
b.arrive_and_wait();
auto now = std::chrono::system_clock::now();
// Locate the current system timezone and convert UTC to local time
auto local_time = std::chrono::current_zone()->to_local(now);
// std::osyncstream guarantees this block of output won't interleave with other threads
std::osyncstream(std::cout)
<< name << " is Starting the task at "
<< std::format("{:%F %T}", local_time) << "\n";
// 4. Simulate performing the task
std::this_thread::sleep_for(std::chrono::milliseconds(timeToFinish));
std::osyncstream(std::cout) << name << " finished the task.\n";
}
};
// ==========================================
// Class classETC
// ==========================================
#include "Student.h"
class classETC {
public:
Student ridit {"Ridit", 1000, 2000};
Student ishan {"Ishan", 3000, 1000};
Student rajdeep {"Rajdeep", 900, 1500};
classETC() = default;
virtual ~classETC() = default;
// FIX: Returns a container of jthreads to main() to keep them alive
auto giveTaskToStudent(std::barrier<>& b) {
std::vector<std::jthread> workers;
workers.reserve(3);
// Emplace threads directly into the vector
workers.emplace_back(&Student::task, &this->ridit, std::ref(b));
workers.emplace_back(&Student::task, &this->ishan, std::ref(b));
workers.emplace_back(&Student::task, &this->rajdeep, std::ref(b));
return workers; // Named Return Value Optimization (NRVO) handles this cleanly
}
};
The Main method
// ==========================================
// Main Method
// ==========================================
#include "classETC.h"
int main() {
// Print C++ standard validation
std::cout << "C++ Version: ";
if (__cplusplus == 202101L) std::cout << "C++23\n";
else if (__cplusplus == 202002L) std::cout << "C++20\n";
else std::cout << "Other/Experimental (" << __cplusplus << ")\n";
std::cout << "-------------------------------------------\n";
std::barrier b(3);
classETC etc;
// FIX: Capturing the threads in main() preserves their lifetimes.
// They will execute concurrently and automatically join when main() ends.
auto threads = etc.giveTaskToStudent(b);
return 0;
}
// ==========================================
// Main Method
// ==========================================
#include "classETC.h"
int main() {
// Print C++ standard validation
std::cout << "C++ Version: ";
if (__cplusplus == 202101L) std::cout << "C++23\n";
else if (__cplusplus == 202002L) std::cout << "C++20\n";
else std::cout << "Other/Experimental (" << __cplusplus << ")\n";
std::cout << "-------------------------------------------\n";
std::barrier b(3);
classETC etc;
// FIX: Capturing the threads in main() preserves their lifetimes.
// They will execute concurrently and automatically join when main() ends.
auto threads = etc.giveTaskToStudent(b);
return 0;
}
The heart of the above concurrent program is
b.arrive_and_wait();
arrive() decrements the participant count.
wait() blocks until the count reaches zero.
arrive_and_wait() combines both.
The Output:
C++ Version: C++20
-------------------------------------------
Ridit is Starting the task at 2026-06-26 00:19:59.803930207
Ishan is Starting the task at 2026-06-26 00:19:59.803930524
Rajdeep is Starting the task at 2026-06-26 00:19:59.803929510
Ishan finished the task.
Rajdeep finished the task.
Ridit finished the task.
Have a look at the time when the three different threads are starting - all of them start at the same time - 00:19:59 - why?
It's because of the barrier!!!
And for the inquisitive mind...
Why is there a mismatch in the starting time in the range of just a few microseconds?
A variation of 1 to 20 microseconds is incredibly tight and proves that our std::barrier system is doing its job perfectly. In a multi-threaded system running on top of a general-purpose operating system (like Linux/Ubuntu), achieving true 0.000000 synchronicity is physically impossible due to these hardware and scheduling realities. To get any closer, we would need a hard Real-Time Operating System (RTOS) with core pinning and disabled interrupts!
Enjoy...
No comments:
Post a Comment