• The wait
statement fails to account for spurious wakeups. A fact of life in threading APIs (in many languages — not just C++) is that code waiting on a condition variable may be awakened even if the condvar wasn't notified. Such awakenings are known as spurious wakeups . Proper code deals with them by confirming that the condition being waited for has truly occurred, and it does this as its first action after waking. The C++ condvar API makes this exceptionally easy, because it permits a lambda (or other function object) that tests for the waited-for condition to be passed to wait
. That is, the wait
call in the reacting task could be written like this:
cv.wait(lk,
[]{ return whether the event has occurred ;});
Taking advantage of this capability requires that the reacting task be able to determine whether the condition it's waiting for is true. But in the scenario we've been considering, the condition it's waiting for is the occurrence of an event that the detecting thread is responsible for recognizing. The reacting thread may have no way of determining whether the event it's waiting for has taken place. That's why it's waiting on a condition variable!
There are many situations where having tasks communicate using a condvar is a good fit for the problem at hand, but this doesn't seem to be one of them.
For many developers, the next trick in their bag is a shared boolean flag. The flag is initially false
. When the detecting thread recognizes the event it's looking for, it sets the flag:
std::atomic flag(false); // shared flag; see
// Item 40 for std::atomic
… // detect event
flag = true; // tell reacting task
For its part, the reacting thread simply polls the flag. When it sees that the flag is set, it knows that the event it's been waiting for has occurred:
… // prepare to react
while (flag);// wait for event
… // react to event
This approach suffers from none of the drawbacks of the condvar-based design. There's no need for a mutex, no problem if the detecting task sets the flag before the reacting task starts polling, and nothing akin to a spurious wakeup. Good, good, good.
Less good is the cost of polling in the reacting task. During the time the task is waiting for the flag to be set, the task is essentially blocked, yet it's still running. As such, it occupies a hardware thread that another task might be able to make use of, it incurs the cost of a context switch each time it starts or completes its time-slice, and it could keep a core running that might otherwise be shut down to save power. A truly blocked task would do none of these things. That's an advantage of the condvar-based approach, because a task in a wait
call is truly blocked.
It's common to combine the condvar and flag-based designs. A flag indicates whether the event of interest has occurred, but access to the flag is synchronized by a mutex. Because the mutex prevents concurrent access to the flag, there is, as Item 40explains, no need for the flag to be std::atomic
; a simple bool
will do. The detecting task would then look like this:
std::condition_variable cv; // as before
std::mutex m;
boolflag(false); // not std::atomic
… // detect event
{
std::lock_guard g(m);// lock m via g's ctor
flag = true; // tell reacting task
// (part 1)
} // unlock m via g's dtor
cv.notify_one(); // tell reacting task
// (part 2)
And here's the reacting task:
… // prepare to react
{ // as before
std::unique_lock lk(m); // as before
cv.wait(lk, [] { return flag; }); // use lambda to avoid
// spurious wakeups
… // react to event
// (m is locked)
}
… // continue reacting
// (m now unlocked)
This approach avoids the problems we've discussed. It works regardless of whether the reacting task wait
s before the detecting task notifies, it works in the presence of spurious wakeups, and it doesn't require polling. Yet an odor remains, because the detecting task communicates with the reacting task in a very curious fashion. Notifying the condition variable tells the reacting task that the event it's been waiting for has probably occurred, but the reacting task must check the flag to be sure. Setting the flag tells the reacting task that the event has definitely occurred, but the detecting task still has to notify the condition variable so that the reacting task will awaken and check the flag. The approach works, but it doesn't seem terribly clean.
An alternative is to avoid condition variables, mutexes, and flags by having the reacting task wait
on a future that's set by the detecting task. This may seem like an odd idea. After all, Item 38 explains that a future represents the receiving end of a communications channel from a callee to a (typically asynchronous) caller, and here there's no callee-caller relationship between the detecting and reacting tasks. However, Item 38also notes that a communications channel whose transmitting end is a std::promise
and whose receiving end is a future can be used for more than just callee-caller communication. Such a communications channel can be used in any situation where you need to transmit information from one place in your program to another. In this case, we'll use it to transmit information from the detecting task to the reacting task, and the information we'll convey will be that the event of interest has taken place.
The design is simple. The detecting task has a std::promise
object (i.e., the writing end of the communications channel), and the reacting task has a corresponding future. When the detecting task sees that the event it's looking for has occurred, it sets the std::promise
(i.e., writes into the communications channel). Meanwhile, the reacting task wait
s on its future. That wait
blocks the reacting task until the std::promise
has been set.
Now, both std::promise
and futures (i.e., std::future
and std::shared_future
) are templates that require a type parameter. That parameter indicates the type of data to be transmitted through the communications channel. In our case, however, there's no data to be conveyed. The only thing of interest to the reacting task is that its future has been set. What we need for the std::promise
and future templates is a type that indicates that no data is to be conveyed across the communications channel. That type is void
. The detecting task will thus use a std::promise
, and the reacting task a std::future
or std::shared_future
. The detecting task will set its std::promise
when the event of interest occurs, and the reacting task will wait
on its future. Even though the reacting task won't receive any data from the detecting task, the communications channel will permit the reacting task to know when the detecting task has “written” its void
data by calling set_value
on its std::promise
.
Читать дальше