by the problem statement. Here is the variant using only one condition variable and broadcast: global volatile RingBuffer queue; // A thread-unsafe ring-buffer of tasks. global Lock queueLock; // A mutex for the ring-buffer of tasks. (Not a spin-lock.) global CV queueFullOrEmptyCV; // A single condition variable for when the queue is not ready for any thread // i.e. for producer threads waiting for the queue to become non-full // and consumer threads waiting for the queue to become non-empty. // Its associated lock is "queueLock". // Not safe to use regular "signal" because it is associated with // multiple predicate conditions (assertions). // Method representing each producer thread's behavior: public method producer() { while (true) { // Producer makes some new task to be added. task myTask = ...; // Acquire "queueLock" for the initial predicate check. queueLock.acquire(); // Critical section that checks if the queue is non-full. while (queue.isFull()) { // Release "queueLock", enqueue this thread onto "queueFullOrEmptyCV" and sleep this thread. wait(queueLock, queueFullOrEmptyCV); // When this thread is awoken, re-acquire "queueLock" for the next predicate check. } // Critical section that adds the task to the queue (note that we are holding "queueLock"). queue.enqueue(myTask); // Wake up all producer and consumer threads that are waiting for the queue to be respectively // non-full and non-empty now that the latter is guaranteed, so that a consumer thread will take the task. broadcast(queueFullOrEmptyCV); // Do not use "signal" (as it might wake up another producer thread only). // End of critical sections. // Release "queueLock" until we need it again to add the next task. queueLock.release(); } } // Method representing each consumer thread's behavior: public method consumer() { while (true) { // Acquire "queueLock" for the initial predicate check. queueLock.acquire(); // Critical section that checks if the queue is non-empty. while (queue.isEmpty()) { // Release "queueLock", enqueue this thread onto "queueFullOrEmptyCV" and sleep this thread. wait(queueLock, queueFullOrEmptyCV); // When this thread is awoken, re-acquire "queueLock" for the next predicate check. } // Critical section that takes a task off of the queue (note that we are holding "queueLock"). myTask = queue.dequeue(); // Wake up all producer and consumer threads that are waiting for the queue to be respectively // non-full and non-empty now that the former is guaranteed, so that a producer thread will add a task. broadcast(queueFullOrEmptyCV); // Do not use "signal" (as it might wake up