template<class Strand>
class basic_condition_variable;
using condition_variable
= basic_condition_variable<boost::asio::io_context::strand>;
As in Boost.Fiber:
The class
condition_variable
provides a mechanism for a fiber to wait for notification from another fiber. When the fiber awakens from the wait, then it checks to see if the appropriate condition is now true, and continues if so. If the condition is not true, then the fiber callswait
again to resume waiting. In the simplest case, this condition is just a boolean variable.
[…] synchronization objects can neither be moved nor copied. A synchronization object acts as a mutually-agreed rendezvous point between different fibers. If such an object were copied somewhere else, the new copy would have no consumers. If such an object were moved somewhere else, leaving the original instance in an unspecified state, existing consumers would behave strangely.
This is a classic synchronization primitive. There’s not much to add. Plenty of documentation and works the same everywhere. Here I’ll mostly only note the differences.
Spurious Wakeups
Boost.Fiber offers the following guarantee:
Neither
condition_variable
norcondition_variable_any
are subject to spurious wakeup:condition_variable::wait()
can only wake up whencondition_variable::notify_one()
orcondition_variable::notify_all()
is called.
This guarantee doesn’t apply to IOFiber. Thanks to a more complex interaction of event flows (we also need to implement interruption requests), IOFiber may on rare occasions perform spurious wakeups. Therefore, the predicate must be re-evaluated upon return.
Wait with a deadline
There is no timedwait()
in this class. You can easily have the equivalent
effect using the interruption API, so it’s redundant. But if a PR is open to add
this feature, I might accept it.
Strands
Objects of this class are protected through a strand (specified in the constructor). The strand must be the same one associated with the mutex whose this condition variable will have a dynamic bind with, otherwise behaviour is undefined.
If the condition variable, the notifier fiber and the waiting fiber all run in the same strand, then there is enough level of determinism to lift one restriction that exists in traditional condition variables.
Even if the shared variable is atomic, it must be modified under the mutex in order to correctly publish the modification to the waiting thread.
The reason why this restriction on the notifier thread exists is to avoid a race. Consider the waiter thread and the notifier thread:
void consumer()
{
std::unique_lock<std::mutex> lk(m);
while (!ready) cond.wait(lk);
// ...
}
void producer()
{
ready = true;
cond.notify_one();
}
Pay attention to the points when the waiter thread checks if the event has been
signalled by testing ready
and the instant it blocks on cond.wait()
. If the
notifier thread mutates the shared variable and calls cond.notify_one()
between these two points, then the signalization is lost. cond.notify_one()
would be called by the time there would be no thread blocked on cond.wait()
.
That’s why the notifier thread need to mutate the shared variable through a
mutex.
If the condition variable, the notifier fiber and the waiting fiber all run in
the same strand, then this restriction isn’t required (as long as there are no
suspension points between the time the waiting fiber tests the condition and
calls cond.wait()
) and the notifier fiber can mutate the shared variable
without holding a lock on the mutex. In this case, the condition variable
essentially becomes a non-suspending way (post semantics) to unpark a parked
fiber.
Member-functions
Constructor
basic_condition_variable(executor_type executor);
Constructs a new condition variable.
Desstructor
~basic_condition_variable();
Destructs the condition variable.
As in Boost.Fiber:
Precondition: All fibers waiting on
*this
have been notified by a call tonotify_one
ornotify_all
(though the respective calls towait
[…] need not have returned).
get_executor()
executor_type get_executor() const;
Returns the strand associated with this condition variable.
wait()
void wait(basic_unique_lock<Strand>& lk, fiber::this_fiber this_fiber);
Atomically call lk.unlock()
and blocks the current fiber.
wait()
is an interruption point. Prior to the delivery of the interruption
request, the underlying mutex is re-acquired under the hood.
notify_*()
void notify_one();
void notify_all();
If any fibers are currently blocked waiting on *this
in a call to wait()
,
unblocks one/all of those fibers.
Nested types
executor_type
using executor_type = Strand;