While locks solve race conditions, they introduce new hazards. If your threads need to acquire multiple locks to perform a task, they can freeze each other in a standoff called a deadlock. We also need ways to let threads coordinate without consuming 100 percent CPU in loops.
Let's look at deadlock avoidance, and thread coordination using condition variables.
Deadlocks: The Polite Standoff
A deadlock occurs when two threads are blocked forever, each waiting for a lock held by the other. For example:
* Thread A locks Mutex A, then tries to lock Mutex B.
* Thread B locks Mutex B, then tries to lock Mutex A.
Both threads wait forever. Neither can release their current lock because they are waiting to acquire the second one.
* The Doorway Metaphor: A deadlock is like two extremely polite people meeting at a narrow doorway. Both step aside to let the other pass, and both refuse to walk through until the other goes first. They stand there frozen face to face forever.
Avoiding Deadlocks with `std::scoped_lock`
To avoid lock acquisition cycles, C++17 introduced std::scoped_lock. It accepts multiple mutexes and locks them all simultaneously using a deadlock avoidance algorithm, ensuring they are always acquired safely:
Condition Variables: Event Signaling
Often, a thread needs to wait for an event (such as a database query to complete or a queue to receive data). If the thread continuously loops checking a variable, it wastes CPU cycles. This is called busy waiting.
Instead, we use a std::condition_variable (from the <condition_variable> header). It allows a thread to sleep until it is explicitly signaled by another thread:
During cv.wait(), the thread automatically releases the mutex lock and enters a sleep state. When cv.notify_one() is called, the OS wakes the thread, and it reacquires the lock before continuing. This prevents CPU busy waiting.