C++20 𝐒𝐞𝐦𝐚𝐩𝐡𝐨𝐫𝐞𝐬
🎯 In this post, I’d like to discuss about another 𝐂++20 𝒄𝒐𝒏𝒄𝒖𝒓𝒓𝒆𝒏𝒄𝒚 𝘧𝘦𝘢𝘵𝘶𝘳𝘦: 𝐒𝐞𝐦𝐚𝐩𝐡𝐨𝐫𝐞𝐬
❗Before we get started, it’s important to note that:
We have different types of 𝐬𝐲𝐧𝐜𝐡𝐫𝐨𝐧𝐢𝐳𝐚𝐭𝐢𝐨𝐧 in C++ multithreading:
1️⃣𝐒𝐲𝐧𝐜𝐡𝐫𝐨𝐧𝐢𝐳𝐚𝐭𝐢𝐨𝐧 𝐏𝐫𝐢𝐦𝐢𝐭𝐢𝐯𝐞𝐬 𝐟𝐨𝐫 𝐌𝐮𝐭𝐚𝐛𝐥𝐞 𝐒𝐡𝐚𝐫𝐞𝐝 𝐃𝐚𝐭𝐚
Mutexes, locks, semaphores, and atomic operations to safely manage access to shared data, preventing data races.
2️⃣𝐒𝐲𝐧𝐜𝐡𝐫𝐨𝐧𝐢𝐳𝐚𝐭𝐢𝐨𝐧 𝐌𝐞𝐜𝐡𝐚𝐧𝐢𝐬𝐦𝐬 𝐟𝐨𝐫 𝐓𝐡𝐫𝐞𝐚𝐝𝐬 𝐂𝐨𝐨𝐫𝐝𝐢𝐧𝐚𝐭𝐢𝐨𝐧
Condition variables, latches, barriers, futures and promises ensure coordinated timing, consistency, and order among threads.
🟣 Now what is 𝐬𝐞𝐦𝐚𝐩𝐡𝐨𝐫𝐞?
The term “𝘴𝘦𝘮𝘢𝘱𝘩𝘰𝘳𝘦” originally referred to a railway signaling system. In concurrency, it is a 𝒍𝒊𝒈𝒉𝒕𝒘𝒆𝒊𝒈𝒉𝒕 𝐬𝐲𝐧𝐜𝐡𝐫𝐨𝐧𝐢𝐳𝐚𝐭𝐢𝐨𝐧 𝐩𝐫𝐢𝐦𝐢𝐭𝐢𝐯𝐞.
A semaphore contains an integer value, initialized in its constructor, which represents 𝐭𝐡𝐞 𝐦𝐚𝐱𝐢𝐦𝐮𝐦 𝐧𝐮𝐦𝐛𝐞𝐫 𝐨𝐟 𝐜𝐨𝐧𝐜𝐮𝐫𝐫𝐞𝐧𝐭 𝐭𝐡𝐫𝐞𝐚𝐝𝐬 that are allowed to access a shared resource, such as 𝘴𝘩𝘢𝘳𝘦𝘥 𝘮𝘶𝘵𝘢𝘣𝘭𝘦 𝘥𝘢𝘵𝘢.
This value is 𝒂𝒄𝒄𝒆𝒔𝒔𝒆𝒅 and 𝒂𝒍𝒕𝒆𝒓𝒆𝒅 exclusively only by two operations:
❇️𝒂𝒄𝒒𝒖𝒊𝒓𝒆 : A thread calls this operation to 𝘦𝘯𝘵𝘦𝘳 𝐚 𝐜𝐫𝐢𝐭𝐢𝐜𝐚𝐥 𝐬𝐞𝐜𝐭𝐢𝐨𝐧.
❇️𝒓𝒆𝒍𝒆𝒂𝒔𝒆: A thread calls this operation to 𝘦𝘹𝘪𝘵 𝐭𝐡𝐞 𝐜𝐫𝐢𝐭𝐢𝐜𝐚𝐥 𝐬𝐞𝐜𝐭𝐢𝐨𝐧.
If the maximum number of threads are already in the critical section, additional threads must wait.
Get Samira’s stories in your inbox
Join Medium for free to get updates from this writer.
There are 𝐭𝐰𝐨 𝐬𝐞𝐦𝐚𝐩𝐡𝐨𝐫𝐞 𝐭𝐲𝐩𝐞𝐬 provided by the C++ standard library:
1️⃣𝐬𝐭𝐝::𝐜𝐨𝐮𝐧𝐭𝐢𝐧𝐠_𝐬𝐞𝐦𝐚𝐩𝐡𝐨𝐫𝐞<> to limit the use of multiple resources up to a maximum value
2️⃣𝐬𝐭𝐝::𝐛𝐢𝐧𝐚𝐫𝐲_𝐬𝐞𝐦𝐚𝐩𝐡𝐨𝐫𝐞<> to limit the use of 𝐚 𝐬𝐢𝐧𝐠𝐥𝐞 𝐫𝐞𝐬𝐨𝐮𝐫𝐜𝐞.
⭕ In this post I’d like to focus on 𝐬𝐭𝐝::𝐛𝐢𝐧𝐚𝐫𝐲_𝐬𝐞𝐦𝐚𝐩𝐡𝐨𝐫𝐞 which is an alias for a 𝒔𝒕𝒅::𝒄𝒐𝒖𝒏𝒕𝒊𝒏𝒈_𝒔𝒆𝒎𝒂𝒑𝒉𝒐𝒓𝒆<1> and can be used like a 𝘴𝘵𝘥::𝘮𝘶𝘵𝘦𝘹.
🔗 Link to a simple example demonstrating a classic 𝐩𝐫𝐨𝐝𝐮𝐜𝐞𝐫-𝐜𝐨𝐧𝐬𝐮𝐦𝐞𝐫 scenario:
lnkd.in/ggQJTQA2
#include <iostream>
#include <thread>
#include <array>
#include <latch>
#include <semaphore>
// Struct to hold shared resources between threads
struct SharedResource
{
static constexpr int BUFFER_SIZE {10};
// Shared buffer
std::array<int, BUFFER_SIZE> data{};
// Semaphores to control the execution flow between producer and consumer
std::binary_semaphore producer{1}; // Indicating that the producer initially can produce a value
std::binary_semaphore consumer{0}; // Indicating that the consumer initially cannot consume a value
// Latch to ensure main waits for both threads (producer and consumer)
// to complete before proceeding
std::latch done{2};
};
// Function executed by the consumer thread
void consumeData(SharedResource& resource)
{
for (int i = 0; i < SharedResource::BUFFER_SIZE; ++i)
{
// Wait for the producer to signal that data is ready
resource.consumer.acquire();
std::cout << "Consumer Reads: " << resource.data[i] << '\n';
// Simulate some processing time
std::this_thread::sleep_for(std::chrono::milliseconds(5));
// Signal the producer to continue production
resource.producer.release();
}
// Signal completion of the consumer thread to the latch
resource.done.count_down();
}
// Function executed by the producer thread
void produceData(SharedResource& resource)
{
for (int i = 0; i < SharedResource::BUFFER_SIZE; ++i)
{
// Wait for the consumer to signal readiness
resource.producer.acquire();
resource.data[i] = i;
std::cout << "Producer writes: " << resource.data[i] << '\n';
// Simulate some processing time
std::this_thread::sleep_for(std::chrono::milliseconds(10));
// Signal the consumer that data is ready
resource.consumer.release();
}
// Signal completion of the producer thread to the latch
resource.done.count_down();
}
int main()
{
SharedResource resource;
std::jthread producer(produceData, std::ref(resource));
std::jthread consumer(consumeData, std::ref(resource));
// Main thread waits here until both threads signal completion via the latch
resource.done.wait();
// Output the data processed by threads
for (int value : resource.data)
{
std::cout << value << " ";
}
return 0;
}💡Unlike mutexes, which enforce 𝐬𝐭𝐫𝐢𝐜𝐭 𝐨𝐰𝐧𝐞𝐫𝐬𝐡𝐢𝐩 𝐚𝐧𝐝 𝐦𝐮𝐭𝐮𝐚𝐥 𝐞𝐱𝐜𝐥𝐮𝐬𝐢𝐨𝐧 (only one thread can access the resource at a time), semaphores provide more 𝐟𝐥𝐞𝐱𝐢𝐛𝐢𝐥𝐢𝐭𝐲. They can allow multiple threads to access the same resource concurrently up to a limit, and the thread that releases the semaphore doesn’t need to be the one that acquired it. For instance, 𝒊𝒏 𝒕𝒉𝒆 𝒆𝒙𝒂𝒎𝒑𝒍𝒆, producer thread can signal consumer thread to proceed, and vice versa.
➡ 𝐑𝐞𝐬𝐨𝐮𝐫𝐜𝐞𝐬:
C++20 The Complete Guide 𝘕𝘪𝘤𝘰𝘭𝘢𝘪 𝘑𝘰𝘴𝘶𝘵𝘵𝘪𝘴
C++20 & Concurrency With Modern C++ 𝘙𝘢𝘪𝘯𝘦𝘳 𝘎𝘳𝘪𝘮𝘮
C++20 for Programmers 3rd edition 𝘗𝘢𝘶𝘭 𝘋𝘦𝘪𝘵𝘦𝘭, 𝘏𝘢𝘳𝘷𝘦𝘺 𝘋𝘦𝘪𝘵𝘦𝘭
Professional C++ 6th Edition 𝘔𝘢𝘳𝘤 𝘎𝘳𝘦𝘨𝘰𝘪𝘳𝘦





