When a lock is not enough
In Module 9, a mutex kept workers from updating a shared counter at the same time. But what if a worker needs data that another worker has not produced yet? Taking a lock protects the data; it does not make the data ready.
We will use a condition variable to wait until shared state allows a worker to proceed. This connects to Chapter 4, §4.8.3, where the book uses condition variables in a barrier. Our producer-and-consumer example isolates the waiting pattern before tackling a whole-group barrier. We will start with pth_ready.c, then extend the idea to repeated handoffs in pth_bounded_buffer.c.
Predict the problem
One worker produces the answer 42; another consumes it. Initially, value = 0 and ready = 0. Both variables are shared.
Discuss: If the consumer locks the mutex first, what value would it read? Would creating the producer first guarantee that the answer is ready? Would a one-second sleep guarantee correctness?
A worker’s creation order does not guarantee its execution order. Sleeping guesses at timing. We need a rule based on the actual state: consume only when ready is true.
Wait for the condition
Create the file with the usual headers. Add:
int value = 0, ready = 0;
pthread_mutex_t lock;
pthread_cond_t available;The consumer function is:
void *consume(void *argument) {
(void)argument;
pthread_mutex_lock(&lock);
while (!ready) {
pthread_cond_wait(&available, &lock);
}
int result = value;
pthread_mutex_unlock(&lock);
printf("Consumer received: %d\n", result);
return NULL;
}Read the wait as three steps:
- Release the mutex and start waiting, as one coordinated operation.
- Wait for a notification; the thread can also wake for other reasons.
- Reacquire the mutex before returning from
pthread_cond_wait.
The worker must hold the mutex before calling wait. Releasing it while waiting lets the producer acquire it and change the shared state. The consumer checks ready again after waking; it proceeds only when the condition is true. This is why we use while, not if. A wake-up is permission to check again, not proof that the data is ready. This pattern follows the POSIX condition-wait specification.
Trace: While the consumer waits, who can hold lock? When wait returns, does the consumer need another lock call?
Produce the value and notify
void *produce(void *argument) {
(void)argument;
int answer = 6 * 7;
pthread_mutex_lock(&lock);
value = answer;
ready = 1;
pthread_cond_signal(&available);
pthread_mutex_unlock(&lock);
return NULL;
}The calculation is local. Publishing value and updating ready use the same mutex as the consumer. The signal notifies a waiter; the consumer can resume through wait only after acquiring the mutex. A condition variable does not hold the answer or copy it to another thread.
If the producer finishes before the consumer starts waiting, the program still works: the consumer sees ready = 1 and skips the wait. The shared flag remembers that the data is ready. A signal by itself is not stored for a future waiter.
Complete main and run
Initialize both synchronization objects before creating workers:
pthread_mutex_init(&lock, NULL);
pthread_cond_init(&available, NULL);Create one consumer and one producer, then join both. Destroy the condition variable and mutex after both joins. Use the complete source to check these calls. As in our earlier examples, we assume successful thread and synchronization calls.
gcc -Wall -pthread -o pth_ready pth_ready.c
./pth_readyExpected output:
Consumer received: 42
Both workers finished
Run several times, then reverse the creation order and rebuild. Both orders should work. Do not add sleeps to arrange the workers.
Trace both possible orders
On paper, trace these cases with columns for producer, consumer, ready, and mutex ownership:
- Consumer acquires the mutex first and waits.
- Producer publishes the answer before the consumer acquires the mutex.
Include every lock, wait, signal, and unlock. Where does the consumer read the answer in each case?
Signal, broadcast, and joining
| Operation | Purpose |
|---|---|
| Mutex | Protect accesses to shared state. |
| Condition wait | Wait for state to become suitable, releasing the mutex while waiting. |
| Condition signal | Notify a waiter. |
| Condition broadcast | Notify all current waiters. |
| Join | Wait for a particular worker to finish. |
pthread_cond_broadcast does not copy data like MPI_Bcast. Threads already share the data; broadcasting wakes waiters so they can reacquire the mutex and check their conditions.
Try: Add a second consumer that reads the same published value. Change the producer’s signal to pthread_cond_broadcast, create and join both consumers, and run it. Each should print 42. Here the value remains available: reading it does not consume or erase it. In a queue where a value can be removed only once, the state and waiting rules would need to account for that.
Second example: a bounded buffer
The first example publishes one value that stays available for reading. Now suppose a producer keeps making values and a consumer processes them one at a time. We need somewhere to hold values that have been produced but not yet consumed. A bounded buffer is a queue with a limited number of slots.
Think of a printer queue or a stream of work for another thread. The producer must not overwrite pending work when the queue is full. The consumer must not remove an item when it is empty. Both threads may need to wait.
Our complete program, pth_bounded_buffer.c, passes the integers 1 through 10, in order, through a buffer with three slots. There is one producer and one consumer. This is a new application of the condition-variable pattern, rather than a copy of the book’s barrier program.
A small queue that wraps around
Start a new file with the usual headers and these shared declarations:
#define CAPACITY 3
#define ITEM_COUNT 10
int buffer[CAPACITY];
int next_put = 0, next_get = 0, used = 0;
pthread_mutex_t lock;
pthread_cond_t not_empty, not_full;next_put is where the producer will insert the next item; next_get is where the consumer will remove the next item. used counts occupied slots. The mutex protects all of this shared state.
After an insertion, advance the index with:
next_put = (next_put + 1) % CAPACITY;With capacity 3, the index goes 0, 1, 2, 0, 1, 2, …. We reuse freed slots instead of shifting the remaining values. The consumer advances next_get in the same way. This is a circular buffer.
Predict: If both indices are 0, is the buffer empty or full? The indices alone cannot tell us: used distinguishes those cases.
Two reasons to wait
| Thread | Wait while | After making progress, notify |
|---|---|---|
| Producer | used == CAPACITY |
not_empty: there is an item to consume. |
| Consumer | used == 0 |
not_full: there is room to produce. |
Both condition variables use the same mutex. As before, every wait releases that mutex while waiting and reacquires it before returning. The while checks the actual state again.
Inside the producer’s loop over items 1 through ITEM_COUNT, write:
pthread_mutex_lock(&lock);
while (used == CAPACITY) {
pthread_cond_wait(¬_full, &lock);
}
buffer[next_put] = item;
next_put = (next_put + 1) % CAPACITY;
used++;
pthread_cond_signal(¬_empty);
pthread_mutex_unlock(&lock);Inside the consumer’s loop, which repeats ITEM_COUNT times, write:
pthread_mutex_lock(&lock);
while (used == 0) {
pthread_cond_wait(¬_empty, &lock);
}
int item = buffer[next_get];
next_get = (next_get + 1) % CAPACITY;
used--;
pthread_cond_signal(¬_full);
pthread_mutex_unlock(&lock);The consumer can print item and add it to a local sum after unlocking. It has its own copy, so the producer can reuse the slot while the consumer processes that copy. We do not need to erase the old number in the array: the indices and used determine which slots contain pending items.
Trace a full buffer
Draw three boxes numbered 0, 1, and 2, then follow this possible sequence. The queue column lists the pending items in removal order, not the physical array order.
| Event | next_put |
next_get |
used |
Queue |
|---|---|---|---|---|
| Start | 0 | 0 | 0 | empty |
| Produce 1 | 1 | 0 | 1 | 1 |
| Produce 2 | 2 | 0 | 2 | 1, 2 |
| Produce 3 | 0 | 0 | 3 | 1, 2, 3 |
| Consume 1 | 0 | 1 | 2 | 2, 3 |
| Produce 4 | 1 | 1 | 3 | 2, 3, 4 |
Discuss: What happens if the producer tries to insert 4 before the consumer removes 1? Which thread makes progress possible? Which physical slot holds 4 afterward? Then trace two more removals and an insertion yourself.
Finish the program and run it
In main, initialize the mutex and both condition variables before creating the workers. Join both workers, then destroy both condition variables and the mutex. Use the complete source to check the setup and the surrounding loops.
gcc -Wall -pthread -o pth_bounded_buffer pth_bounded_buffer.c
./pth_bounded_bufferYou should see Consumed: 1 through Consumed: 10, followed by Sum: 55 and Check: PASS. Check the sequence as well as the sum: a total alone would not detect every possible duplication or ordering mistake.
The hand trace shows one possible schedule. A run may interleave production and consumption differently; it does not have to fill all three slots before consuming anything.
Try: Change capacity to 1, then 5, rebuilding each time. Does the consumed sequence change? Does a larger buffer automatically guarantee a faster program? Reverse the thread-creation order and check again. No sleeps are needed.
This example knows in advance that exactly ten items will be produced and consumed. Adding another consumer that also tries to remove ten items would request twenty items and leave threads waiting. A more general queue needs a rule for deciding when no more items will arrive; we can add that later.
Check your understanding
- Why does waiting while holding an ordinary mutex, without releasing it, block progress?
- What remembers that the answer is ready if the signal comes early?
- Why is the condition tested in a loop?
- Why can printing happen after unlocking in our consumer?
- The producer needs that mutex to publish the value. Condition wait releases it while waiting.
- The shared
readyflag, accessed under the mutex. - A notification or other wake-up does not establish that the condition is true; recheck it under the lock.
- The consumer copied the value into its own local
result. It no longer needs to access the shared value for printing.