Condition variables
A thread that has to wait for another one has two bad options and one good one. It can spin on a flag and burn a core, or sleep for a guessed time and be late; a condition variable puts it to sleep until a thread that changed the state wakes it.
The mutex of lesson 106 protects the state. The condition variable is the second half: wait releases the mutex, sleeps, and takes it back before it returns, so the predicate is always checked under the lock.
A condition variable is cheap while it waits: the thread sleeps in the kernel until a notification arrives. That is why wait is preferred to a loop that checks an atomic flag, which spends a core on every check and still needs the same predicate.
The two variables in the lesson are one pattern and its limit: a queue with a predicate is the general case, and a single flag is the special case where one value is the whole message.
wait and notify_one
The consumer locks the mutex and calls wait with a predicate. The call unlocks the mutex, blocks until a notification arrives, locks again and evaluates the predicate; the wait ends only when the predicate is true.
The producer takes the same mutex, changes the value and releases it, then calls notify_one. The order matters: the state changes before the notification, so a consumer that wakes sees the new value.
Without the predicate, wait can return from a spurious wakeup and the code would read a value that is still zero. The predicate turns the wait into a loop that only ends when there is something to see.
#include <condition_variable>
#include <iostream>
#include <mutex>
#include <thread>
std::mutex guard;
std::condition_variable ready;
int value = 0;
int main()
{
std::thread consumer{[] {
std::unique_lock<std::mutex> lock{guard};
ready.wait(lock, [] { return value != 0; });
std::cout << value << '\n';
}};
{
std::lock_guard<std::mutex> lock{guard};
value = 42;
}
ready.notify_one();
consumer.join();
}
g++ -std=c++23 -Wall -Wextra -pthread wait.cpp -o wait
./wait
42
A queue with a predicate
A queue is the usual shape: the consumer waits while the queue is empty and takes one item per wakeup. The predicate is !work.empty(), which is also the condition that makes the pop safe.
The producer pushes five numbers and notifies after each push. The consumer adds them and prints 15, and the number of wakeups is irrelevant: a wakeup that finds the queue non-empty is enough.
The loop of the consumer waits five times because it consumes five items. A consumer that loops until the queue is empty and the producer is done needs a second flag, which is the shutdown condition of lesson 112.
#include <condition_variable>
#include <iostream>
#include <mutex>
#include <queue>
std::mutex guard;
std::condition_variable has_work;
std::queue<int> work;
void produce()
{
for (int i = 1; i <= 5; ++i) {
{
std::lock_guard<std::mutex> lock{guard};
work.push(i);
}
has_work.notify_one();
}
}
int consume()
{
int total = 0;
for (int i = 0; i < 5; ++i) {
std::unique_lock<std::mutex> lock{guard};
has_work.wait(lock, [] { return !work.empty(); });
total += work.front();
work.pop();
}
return total;
}
int main()
{
int total = 0;
std::thread worker{[&] { total = consume(); }};
produce();
worker.join();
std::cout << total << '\n';
}
g++ -std=c++23 -Wall -Wextra -pthread queue.cpp -o queue
./queue
15
A signal for every waiter
notify_one wakes one waiter and leaves the others asleep. notify_all wakes every thread that waits on the variable, and each one then competes for the mutex and checks its predicate.
The program starts three workers that wait for a single go flag. Main sets the flag under the lock and calls notify_all, and all three wake, increment the counter and exit. The print is 3.
A loop that waits for go and then works would need the flag to stay true, and a worker that has to wait again would need a counter or a generation number. The predicate is the state, and the state decides how many waiters can pass.
#include <condition_variable>
#include <iostream>
#include <mutex>
#include <thread>
#include <vector>
std::mutex guard;
std::condition_variable start;
bool go = false;
int done = 0;
int main()
{
std::vector<std::thread> workers;
for (int i = 0; i < 3; ++i) {
workers.emplace_back([] {
std::unique_lock<std::mutex> lock{guard};
start.wait(lock, [] { return go; });
++done;
});
}
{
std::lock_guard<std::mutex> lock{guard};
go = true;
}
start.notify_all();
for (auto& worker : workers) {
worker.join();
}
std::cout << done << '\n';
}
g++ -std=c++23 -Wall -Wextra -pthread all.cpp -o all
./all
3
The lock wait cannot use
wait has to unlock the mutex and lock it again, so it needs a lock it can control. A std::lock_guard cannot be unlocked early, and a condition variable therefore refuses it at compile time.
The program waits on a lock_guard and the compiler reports that no overload of wait matches. The candidates take a std::unique_lock, which is the type the lesson used in every earlier section.
The fix is one word: declare the lock as std::unique_lock<std::mutex>. The second argument of wait is the predicate, and a wait without one is the version that wakes up for no reason.
#include <condition_variable>
#include <mutex>
int main()
{
std::mutex guard;
std::condition_variable ready;
std::lock_guard<std::mutex> lock{guard};
ready.wait(lock);
}
g++ -std=c++23 -Wall -Wextra -pthread guard.cpp -o guard
guard.cpp: In function 'int main()':
guard.cpp:10:15: error: no matching function for call to 'std::condition_variable::wait(std::lock_guard<std::mutex>&)'
10 | ready.wait(lock);
| ~~~~~~~~~~^~~~~~
guard.cpp:10:15: note: there are 2 candidates
In file included from guard.cpp:1:
/usr/include/c++/15.2.0/condition_variable:100:5: note: candidate 1: 'void std::condition_variable::wait(std::unique_lock<std::mutex>&)'
100 | wait(unique_lock<mutex>& __lock);
| ^~~~
/usr/include/c++/15.2.0/condition_variable:100:30: note: no known conversion for argument 1 from 'std::lock_guard<std::mutex>' to 'std::unique_lock<std::mutex>&'
100 | wait(unique_lock<mutex>& __lock);
| ~~~~~~~~~~~~~~~~~~~~^~~~~~
/usr/include/c++/15.2.0/condition_variable:104:7: note: candidate 2: 'template<class _Predicate> void std::condition_variable::wait(std::unique_lock<std::mutex>&, _Predicate)'
104 | wait(unique_lock<mutex>& __lock, _Predicate __p)
| ^~~~
/usr/include/c++/15.2.0/condition_variable:104:7: note: candidate expects 2 arguments, 1 provided
Exercises
- Add a second consumer to the queue program and check that the total is still 15.
- Give the queue a limit of two items and make the producer wait while it is full.
- Remove the predicate from the wait in wait.cpp and explain why the program can now print 0.
Questions
A new set of questions in a new order every time. Choose one answer for each, then check.
-
What does
wait(lock, predicate)do while the predicate is false?Answer
It unlocks the mutex, sleeps, and locks the mutex again before checking
The unlock lets the producer take the mutex; the wakeup takes it back before the predicate is evaluated.
-
Why is the predicate checked while the mutex is held?
Answer
So the state cannot change between the check and the action taken on it
The mutex makes the check and the following read of the shared state one critical section.
-
What does
notify_onewake?Answer
One thread that waits on the variable
notify_one wakes a single waiter; notify_all wakes every waiter.
-
What does this program print?
#include <condition_variable> #include <iostream> #include <mutex> #include <thread> std::mutex m; std::condition_variable cv; int value = 0; int main() { std::thread consumer([]{ std::unique_lock<std::mutex> lock{m}; cv.wait(lock, []{ return value != 0; }); std::cout << value; }); { std::lock_guard<std::mutex> lock{m}; value = 42; } cv.notify_one(); consumer.join(); }Answer
42
The consumer waits for value to change, the producer stores 42 and notifies, and the consumer prints it.
-
What does this program print?
#include <condition_variable> #include <iostream> #include <mutex> #include <queue> std::mutex m; std::condition_variable cv; std::queue<int> work; int total = 0; int main() { std::thread consumer([]{ for (int i = 0; i < 5; ++i) { std::unique_lock<std::mutex> lock{m}; cv.wait(lock, []{ return !work.empty(); }); total += work.front(); work.pop(); } }); for (int i = 1; i <= 5; ++i) { { std::lock_guard<std::mutex> lock{m}; work.push(i); } cv.notify_one(); } consumer.join(); std::cout << total; }Answer
15
The consumer adds 1 through 5, and the queue keeps the order.
-
What does this program print?
#include <condition_variable> #include <iostream> #include <mutex> #include <thread> std::mutex m; std::condition_variable cv; bool go = false; int done = 0; int main() { auto wait = []{ std::unique_lock<std::mutex> lock{m}; cv.wait(lock, []{ return go; }); ++done; }; std::thread a(wait), b(wait), c(wait); { std::lock_guard<std::mutex> lock{m}; go = true; } cv.notify_all(); a.join(); b.join(); c.join(); std::cout << done; }Answer
3
notify_all wakes the three waiters, and each increments the counter once.
-
What happens when
notify_oneis called before any thread waits?Answer
The notification is lost and a later wait can block forever
A condition variable has no memory: a notification with no waiter is gone, which is why the state and the predicate must carry it.
-
Which lock type does
waitrequire?Answer
std::unique_lock<std::mutex>wait must unlock and relock, so it needs the lock that can do that.
-
What can a wait without a predicate return from?
Answer
A spurious wakeup, with the condition still false
The standard allows wakeups that no notification caused, so the predicate is what makes the wait correct.
-
A producer pushes an item while holding a mutex. What does it do immediately after the push?
Answer
It releases the mutex and then calls notify_one
The push is inside a block that ends before the notification, so the consumer never wakes to a mutex the producer still holds.
The code on this page was built with g++ 15.2.0 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container. The programs whose section shows a compiler error were compiled until that error and were not run.
Article author: Arthur Isaev