DEV Community

Somenath Mukhopadhyay
Somenath Mukhopadhyay

Posted on • Edited on

The Barrier in C++ 20 concurrency - the programmer in me is still thriving...

Enjoy my training video on the C++ barrier...

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";
    }
};

Enter fullscreen mode Exit fullscreen mode

Class classETC


// ==========================================
// 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
    }
};

Enter fullscreen mode Exit fullscreen mode
### 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;
}
Enter fullscreen mode Exit fullscreen mode

# The Output:
Rajdeep is Starting the task at 2026-06-27 11:45:30.400862925
Ishan is Starting the task at 2026-06-27 11:45:30.400863712
Ridit is Starting the task at 2026-06-27 11:45:30.400881538
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 - 11:45:30 - why?

As you have guessed it correctly, 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...

Top comments (0)