C++
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented C++
-
Section 4: Memory Management
-
Section 5: Templates and Generic Programming
-
Section 6: The Standard Template Library
-
Section 7: Modern C++ Features
-
Section 8: Error Handling
-
Section 9: Multithreading
-
Section 10: Operator Overloading and Type Conversion
-
Section 11: Advanced Topics
-
Section 12: Tooling and Build Systems
-
Section 13: Design Patterns in C++
-
Section 14: Interfacing with C and Systems Programming
-
Section 15: Networking and IPC Basics
-
Section 16: Graphics and Game Programming Basics
-
Section 17: The Boost Libraries
-
Section 18: Data Structures and Algorithms in C++
-
Section 19: Practical Projects
-
Section 20: More Concurrency Patterns
-
Section 21: More OOP and Design Practice
-
Section 22: File I/O and Streams
-
Section 23: More Standard Library
-
Section 24: Practice Exercises
-
Section 25: Interview and Algorithm Practice
-
Section 26: Compiler and Language Internals
-
Section 27: GUI and Application Frameworks Overview
-
Section 28: Testing and Quality Practices
-
Section 29: Numerics Library
-
Section 30: Concepts Library (C++20)
-
Section 31: Ranges Library (C++20) In Depth
-
Section 32: More Utility Library
109: Condition Variables
I see this mistake almost every time I review code from developers moving into multi-threaded C++: they treat std::condition_variable::notify_one() like a reliable postal service. They assume that if they call notify, the waiting thread will wake up, see that the work is ready, and get to it. They often write code like this: cv.wait(lock); without any surrounding loop or predicate.
Thinking notify_one() is a Guarantee
The misconception is that the notification itself carries the "truth" of the condition. In reality, a condition variable is just a signaling mechanism, not a state container. If you just call wait(), your thread might wake up even if nobody called notify—this is called a "spurious wakeup." It's a quirk of how threading APIs are implemented across different operating systems. Even worse, if you have two threads waiting and one wakes up, it might grab the data and finish the job before the second thread even fully wakes up. If that second thread doesn't check the state again, it'll try to process data that isn't there, and your program will crash.
The Necessity of the Predicate Loop
To handle this, we never wait in a vacuum. We always wait on a predicate—a boolean expression that defines exactly what "ready" looks like. C++ gives us a convenient overload for wait() that takes a lambda. Under the hood, this lambda is wrapped in a while loop. I always tell my juniors: if you aren't checking the condition in a loop, you're just gambling with your stability.
Let's look at a concrete example. Imagine we're building a background job queue for a game engine. We have a producer pushing tasks and a worker thread waiting to execute them.
#include <mutex>
#include <condition_variable>
#include <queue>
#include <string>
#include <iostream>
class JobQueue {
std::queue<std::string> tasks;
std::mutex mtx;
std::condition_variable cv;
public:
void push_job(const std::string& job) {
{
std::lock_guard<std::mutex> lock(mtx);
tasks.push(job);
}
// Notify after releasing the lock to avoid "hurry up and wait"
// where the woken thread immediately blocks on the mutex we still hold.
cv.notify_one();
}
std::string pop_job() {
std::unique_lock<std::mutex> lock(mtx);
// This is the critical part. The lambda [this]{ return !tasks.empty(); }
// ensures that if we wake up spuriously, we check the queue and go back
// to sleep if it's actually still empty.
cv.wait(lock, [this] { return !tasks.empty(); });
std::string job = tasks.front();
tasks.pop();
return job;
}
};
Notice that I used std::unique_lock in pop_job. You can't use std::lock_guard here because cv.wait() needs to be able to unlock the mutex while it sleeps and relock it when it wakes up. lock_guard is too simple for that; it only unlocks when it goes out of scope.
Managing the Wake-up Storm
One final tip: choose between notify_one() and notify_all() carefully. If you have ten worker threads waiting on a queue and you push one single job, notify_one() is efficient. If you use notify_all(), you wake up all ten threads, but nine of them will immediately realize the queue is empty (thanks to that predicate loop we wrote) and go back to sleep. This is called a "thundering herd" problem, and it can absolutely tank your performance in high-throughput systems.
📋 Practical Task
Build a Thread-Safe Print Spooler
Your task is to implement a PrintSpooler class that manages a set of documents waiting to be printed. Multiple "Application" threads will be submitting documents, and one "Printer" thread will be processing them one by one.
Requirements:
- Implement a
submit_document(std::string doc)method that adds a document to a internal queue and notifies the printer. - Implement a
process_next_document()method that blocks the printer thread until a document is available. - You must use
std::condition_variablewith a predicate lambda to prevent spurious wakeups. - Ensure the
process_next_document()method returns the document string once it has been successfully popped from the queue. - Use
std::unique_lockandstd::mutexto ensure thread safety.
Test your implementation by spawning three threads that each submit five documents, and one worker thread that prints "Printing: [doc name]" in a loop until a specific "SHUTDOWN" signal is received.
There are no comments for now.