Deadlocks and Condition Variables
Concurrency and Multithreading

7.3 Deadlocks and Condition Variables

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:

#include <mutex>

std::mutex mutexA;
std::mutex mutexB;

void safeTransfer() {
    // Locks both mutexes simultaneously without deadlocking!
    std::scoped_lock lock{mutexA, mutexB};
    // Perform transfer operations...
}
Locking multiple mutexes safely using std::scoped_lock.

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:

#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>

std::mutex mtx;
std::condition_variable cv;
bool ready{false};

void waitForWork() {
    std::unique_lock<std::mutex> lock{mtx};
    
    // Sleep until ready becomes true. Releases the lock while sleeping!
    cv.wait(lock, []{ return ready; });
    
    std::cout << "Worker thread: Signal received! Performing task...\n";
}

int main() {
    std::thread worker{waitForWork};
    
    // Simulate preparing work
    std::this_thread::sleep_for(std::chrono::seconds(2));
    
    {
        std::lock_guard<std::mutex> lock{mtx};
        ready = true;
    }
    cv.notify_one(); // Wake up the sleeping worker thread!
    
    worker.join();
    return 0;
}
Safe thread signaling with condition variables.

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.

Finished reading this lesson?