the buffer, the "number of empty positions" semaphore counts the empty locations in the buffer and the semaphore "buffer manipulation" works as mutex for the buffer put and get operations. If the buffer is full, that is number of empty positions is zero, the producer thread will wait in the P(number of empty positions) operation. If the buffer is empty, that is the number of queueing portions is zero, the consumer thread will wait in the P(number of queueing portions) operation. The V() operations release the semaphores. As a side effect, a thread can move from the wait queue to the ready queue. The P() operation decreases the semaphore value down to zero. The V() operation increases the semaphore value. begin integer number of queueing portions, number of empty positions, buffer manipulation; number of queueing portions:= 0; number of empty positions:= N; buffer manipulation:= 1; parbegin producer: begin again 1: produce next portion; P(number of empty positions); P(buffer manipulation); add portion to buffer; V(buffer manipulation); V(number of queueing portions); goto again 1 end; consumer: begin again 2: P(number of queueing portions); P(buffer manipulation); take portion from buffer; V(buffer manipulation) ; V(number of empty positions); process portion taken; goto again 2 end parend end As of C++ 20, semaphores are part of the language. Dijkstra's solution can easily be written in modern C++. The variable buffer_manipulation is a mutex. The semaphore feature of acquiring in one thread and releasing in another thread is not needed. The lock_guard() statement instead of a lock() and unlock() pair is C++ RAII. The lock_guard destructor ensures lock release in case of an exception. This solution can handle multiple consumer threads and/or multiple producer threads. #include <thread> #include <mutex> #include <semaphore> std::counting_semaphore<N> number_of_queueing_portions{0}; std::counting_semaphore<N> number_of_empty_positions{N}; std::mutex buffer_manipulation; void producer() { for (;;) { Portion portion = produce_next_portion(); number_of_empty_positions.acquire(); { std::lock_guard<std::mutex> g(buffer_manipulation); add_portion_to_buffer(portion); } number_of_queueing_portions.release(); } } void consumer() { for (;;) { number_of_queueing_portions.acquire(); Portion portion; { std::lock_guard<std::mutex> g(buffer_manipulation); portion = take_portion_from_buffer(); } number_of_empty_positions.release(); process_portion_taken(portion); } } int main() { std::thread t1(producer); std::thread t2(consumer); t1.join(); t2.join(); } Using monitors Per Brinch Hansen defined the monitor: I will use the term monitor to denote a shared variable and the set of meaningful operations on it. The purpose of a monitor is to control the scheduling of resources among individual processes according to a certain policy. Tony Hoare laid a theoretical foundation for the monitor. bounded buffer: monitor begin