This section serves as a critical bridge between understanding Mutexes (used for protection) and Condition Variables (used for signaling/waiting). It demonstrates why mutexes alone are insufficient for complex synchronization tasks.
1. Core Objective: Locking Waiting
- Locking: Protecting a critical region so that only one thread can access shared data at a time.
- Waiting: Pausing execution until a specific condition (like data becoming available) becomes true.
Stevens uses this section to demonstrate that while Mutexes are excellent for locking, they are terrible mechanisms for waiting.
2. The Scenario: Concurrent Producer-Consumer
In the previous section (7.3), the example program (prodcons2) was simple: the consumer thread didn’t start until all producer threads had finished. This avoided race conditions regarding data availability but isn’t realistic for real-world applications where producers and consumers run simultaneously.
In Section 7.4, the code is modified (prodcons3) to start the consumer thread immediately after the producer threads are created.
Figure 7.4: Main Function Changes
- Objective: Start the consumer while producers are still running.
- Logic:
- The program creates multiple producer threads.
- Immediately after, it creates the consumer thread
Pthread_create(&tid_consume, ...). - It then waits for everyone to finish.
This creates a synchronization problem: What if the consumer tries to process buff[0] before a producer has written to it?
3. The “Spinning” Solution (and why it fails)
To solve this, the text introduces a temporary (and inefficient) solution in Figure 7.5, specifically the consume_wait function. This function attempts to use a mutex to “wait” for data.
Analysis of consume_wait (Figure 7.5)
The consumer needs to wait until the index i (the item it wants) has been produced (indicated by shared.nput).
void consume_wait(int i) {
for (;;) {
Pthread_mutex_lock(&shared.mutex); // 1. Lock the mutex
if (i < shared.nput) { // 2. Check the condition
Pthread_mutex_unlock(&shared.mutex);
return; // 3. Item is ready! Return.
}
Pthread_mutex_unlock(&shared.mutex); // 4. Not ready? Unlock...
} // 5. ...and LOOP (Spin)
}The Logic Flaw:
- The code acquires the lock to check if the data is ready.
- If the data is not ready, it releases the lock and immediately tries again (loops back to step 1).
- This behavior is called Spinning or Polling.
4. Why Spinning is Bad
The text explicitly condemns this approach for two reasons:
- Waste of CPU: The consumer thread runs in a tight loop, constantly locking and unlocking the mutex, checking a value, and repeating. This burns CPU cycles doing nothing productive.
- Contention: By constantly locking the mutex, the consumer might make it harder for the producer to get the lock to actually write the data, slowing down the very process it is waiting for.