...one of the most highly
regarded and expertly designed C++ library projects in the
world.
— Herb Sutter and Andrei
Alexandrescu, C++
Coding Standards
The simplest case is when you only need to know that the first of a set of asynchronous tasks has completed — but you don't need to obtain a return value, and you're confident that they will not throw exceptions.
For this we introduce a Done
class to wrap a bool
variable
with a condition_variable
and a mutex
:
// Wrap canonical pattern for condition_variable + bool flag struct Done { private: boost::fibers::condition_variable cond; boost::fibers::mutex mutex; bool ready = false; public: typedef std::shared_ptr< Done > ptr; void wait() { std::unique_lock< boost::fibers::mutex > lock( mutex); cond.wait( lock, [this](){ return ready; }); } void notify() { { std::unique_lock< boost::fibers::mutex > lock( mutex); ready = true; } // release mutex cond.notify_one(); } };
The pattern we follow throughout this section is to pass a std::shared_ptr<>
to the relevant synchronization object to the various tasks' fiber functions.
This eliminates nagging questions about the lifespan of the synchronization
object relative to the last of the fibers.
wait_first_simple()
uses that tactic for Done
:
template< typename ... Fns > void wait_first_simple( Fns && ... functions) { // Use shared_ptr because each function's fiber will bind it separately, // and we're going to return before the last of them completes. auto done( std::make_shared< Done >() ); wait_first_simple_impl( done, std::forward< Fns >( functions) ... ); done->wait(); }
wait_first_simple_impl()
is an ordinary recursion over the argument
pack, capturing Done::ptr
for each new fiber:
// Degenerate case: when there are no functions to wait for, return // immediately. void wait_first_simple_impl( Done::ptr) { } // When there's at least one function to wait for, launch it and recur to // process the rest. template< typename Fn, typename ... Fns > void wait_first_simple_impl( Done::ptr done, Fn && function, Fns && ... functions) { boost::fibers::fiber( [done, function](){ function(); done->notify(); }).detach(); wait_first_simple_impl( done, std::forward< Fns >( functions) ... ); }
The body of the fiber's lambda is extremely simple, as promised: call the
function, notify Done
when it returns. The first fiber to do so allows wait_first_simple()
to return — which is why it's useful to
have std::shared_ptr<Done>
manage the lifespan of our Done
object rather than declaring it as a stack variable in wait_first_simple()
.
This is how you might call it:
wait_first_simple( [](){ sleeper("wfs_long", 150); }, [](){ sleeper("wfs_medium", 100); }, [](){ sleeper("wfs_short", 50); });
In this example, control resumes after wait_first_simple()
when sleeper("wfs_short",
50)
completes — even though the other two sleeper()
fibers are still running.