OliverKowalke2013Oliver Kowalke
Distributed under the Boost Software License, Version 1.0. (See accompanying
file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
C++ Library to cooperatively schedule and synchronize micro-threads
FiberOverviewBoost.Fiber provides a framework for micro-/userland-threads
(fibers) scheduled cooperatively. The API contains classes and functions to
manage and synchronize fibers similiarly to standard
thread support library.
Each fiber has its own stack.
A fiber can save the current execution state, including all registers and CPU
flags, the instruction pointer, and the stack pointer and later restore this
state. The idea is to have multiple execution paths running on a single thread
using cooperative scheduling (versus threads, which are preemptively scheduled).
The running fiber decides explicitly when it should yield to allow another
fiber to run (context switching). Boost.Fiber
internally uses call/cc
from Boost.Context;
the classes in this library manage, schedule and, when needed, synchronize
those execution contexts. A context switch between threads usually costs thousands
of CPU cycles on x86, compared to a fiber switch with less than a hundred cycles.
A fiber runs on a single thread at any point in time.
In order to use the classes and functions described here, you can either include
the specific headers specified by the descriptions of each class or function,
or include the master library header:
#include<boost/fiber/all.hpp>
which includes all the other headers in turn.
The namespaces used are:
namespaceboost::fibersnamespaceboost::this_fiberFibers
and Threads
Control is cooperatively passed between fibers launched on a given thread.
At a given moment, on a given thread, at most one fiber is running.
Spawning additional fibers on a given thread does not distribute your program
across more hardware cores, though it can make more effective use of the core
on which it's running.
On the other hand, a fiber may safely access any resource exclusively owned
by its parent thread without explicitly needing to defend that resource against
concurrent access by other fibers on the same thread. You are already guaranteed
that no other fiber on that thread is concurrently touching that resource.
This can be particularly important when introducing concurrency in legacy code.
You can safely spawn fibers running old code, using asynchronous I/O to interleave
execution.
In effect, fibers provide a natural way to organize concurrent code based on
asynchronous I/O. Instead of chaining together completion handlers, code running
on a fiber can make what looks like a normal blocking function call. That call
can cheaply suspend the calling fiber, allowing other fibers on the same thread
to run. When the operation has completed, the suspended fiber resumes, without
having to explicitly save or restore its state. Its local stack variables persist
across the call.
A fiber can be migrated from one thread to another, though the library does
not do this by default. It is possible for you to supply a custom scheduler
that migrates fibers between threads. You may specify custom fiber properties
to help your scheduler decide which fibers are permitted to migrate. Please
see Migrating fibers between threads and
Customization for more details.
Boost.Fiber allows to multiplexfibersacrossmultiplecores (see numa::work_stealing).
A fiber launched on a particular thread continues running on that thread unless
migrated. It might be unblocked (see Blocking
below) by some other thread, but that only transitions the fiber from blocked
to ready on its current thread — it does not cause the fiber to
resume on the thread that unblocked it.
thread-local
storage
Unless migrated, a fiber may access thread-local storage; however that storage
will be shared among all fibers running on the same thread. For fiber-local
storage, please see fiber_specific_ptr.
BOOST_FIBERS_NO_ATOMICS
The fiber synchronization objects provided by this library will, by default,
safely synchronize fibers running on different threads. However, this level
of synchronization can be removed (for performance) by building the library
with BOOST_FIBERS_NO_ATOMICS
defined. When the library is built with that macro, you must ensure that all
the fibers referencing a particular synchronization object are running in the
same thread. Please see Synchronization.
Blocking
Normally, when this documentation states that a particular fiber blocks
(or equivalently, suspends), it means that it yields control,
allowing other fibers on the same thread to run. The synchronization mechanisms
provided by Boost.Fiber have this behavior.
A fiber may, of course, use normal thread synchronization mechanisms; however
a fiber that invokes any of these mechanisms will block its entire thread,
preventing any other fiber from running on that thread in the meantime. For
instance, when a fiber wants to wait for a value from another fiber in the
same thread, using std::future would be unfortunate: std::future::get()
would block the whole thread, preventing the other fiber from delivering its
value. Use future<> instead.
Similarly, a fiber that invokes a normal blocking I/O operation will block
its entire thread. Fiber authors are encouraged to consistently use asynchronous
I/O. Boost.Asio
and other asynchronous I/O operations can straightforwardly be adapted for
Boost.Fiber: see Integrating
Fibers with Asynchronous Callbacks.
Boost.Fiber depends upon Boost.Context.
Boost version 1.61.0 or greater is required.
This library requires C++11!
Implementations:
fcontext_t, ucontext_t and WinFiberBoost.Fiber uses call/cc
from Boost.Context
as building-block.
fcontext_t
The implementation uses fcontext_t
per default. fcontext_t is based on assembler and not available for all platforms.
It provides a much better performance than ucontext_t
(the context switch takes two magnitudes of order less CPU cycles; see section
performance)
and WinFiber.
ucontext_t
As an alternative, ucontext_t can be used by compiling
with BOOST_USE_UCONTEXT and
b2 property context-impl=ucontext.
ucontext_t might be available
on a broader range of POSIX-platforms but has some disadvantages
(for instance deprecated since POSIX.1-2003, not C99 conform).
call/cc
supports Segmented stacks
only with ucontext_t as
its implementation.
WinFiber
With BOOST_USE_WINFIB and
b2 property context-impl=winfib
Win32-Fibers are used as implementation for call/cc.
Because the TIB (thread information block) is not fully described in the
MSDN, it might be possible that not all required TIB-parts are swapped.
The first call of call/cc
converts the thread into a Windows fiber by invoking ConvertThreadToFiber(). If desired, ConvertFiberToThread() has to be called by the user explicitly
in order to release resources allocated by ConvertThreadToFiber() (e.g. after using boost.context).
Windows using fcontext_t: turn off global program optimization (/GL) and
change /EHsc (compiler assumes that functions declared as extern "C"
never throw a C++ exception) to /EHs (tells compiler assumes that functions
declared as extern "C" may throw an exception).
Fiber managementSynopsis
#include<boost/fiber/all.hpp>namespaceboost{namespacefibers{classfiber;booloperator<(fiberconst&l,fiberconst&r)noexcept;voidswap(fiber&l,fiber&r)noexcept;template<typenameSchedAlgo,typename...Args>voiduse_scheduling_algorithm(Args&&...args);boolhas_ready_fibers();namespacealgo{structalgorithm;template<typenamePROPS>structalgorithm_with_properties;classround_robin;classshared_round_robin;}}namespacethis_fiber{fibers::idget_id()noexcept;voidyield();template<typenameClock,typenameDuration>voidsleep_until(std::chrono::time_point<Clock,Duration>const&abs_time)template<typenameRep,typenamePeriod>voidsleep_for(std::chrono::duration<Rep,Period>const&rel_time);template<typenamePROPS>PROPS&properties();}Tutorial
Each fiber represents a micro-thread which will be launched and managed
cooperatively by a scheduler. Objects of type fiber are move-only.
boost::fibers::fiberf1;// not-a-fibervoidf(){boost::fibers::fiberf2(some_fn);f1=std::move(f2);// f2 moved to f1}Launching
A new fiber is launched by passing an object of a callable type that can be
invoked with no parameters. If the object must not be copied or moved, then
std::ref can be used to pass in a reference to the function
object. In this case, the user must ensure that the referenced object outlives
the newly-created fiber.
structcallable{voidoperator()();};boost::fibers::fibercopies_are_safe(){callablex;returnboost::fibers::fiber(x);}// x is destroyed, but the newly-created fiber has a copy, so this is OKboost::fibers::fiberoops(){callablex;returnboost::fibers::fiber(std::ref(x));}// x is destroyed, but the newly-created fiber still has a reference// this leads to undefined behaviour
The spawned fiber does not immediately start running. It is enqueued
in the list of ready-to-run fibers, and will run when the scheduler gets around
to it.
Exceptions
An exception escaping from the function or callable object passed to the fiber
constructor
calls std::terminate().
If you need to know which exception was thrown, use future<> or
packaged_task<>.
Detaching
A fiber can be detached by explicitly invoking the fiber::detach() member
function. After fiber::detach() is called on a fiber object, that
object represents not-a-fiber. The fiber object may then
safely be destroyed.
boost::fibers::fiber(some_fn).detach();Boost.Fiber provides a number of ways to wait
for a running fiber to complete. You can coordinate even with a detached fiber
using a mutex, or condition_variable, or
any of the other synchronization objects
provided by the library.
If a detached fiber is still running when the thread’s main fiber terminates,
the thread will not shut down.
Joining
In order to wait for a fiber to finish, the fiber::join() member function
of the fiber object can be used. fiber::join() will block
until the fiber object has completed.
voidsome_fn(){...}boost::fibers::fiberf(some_fn);...f.join();
If the fiber has already completed, then fiber::join() returns immediately
and the joined fiber object becomes not-a-fiber.
Destruction
When a fiber object representing a valid execution context (the fiber
is fiber::joinable()) is destroyed, the program terminates. If
you intend the fiber to outlive the fiber object that launched it,
use the fiber::detach() method.
{boost::fibers::fiberf(some_fn);}// std::terminate() will be called{boost::fibers::fiberf(some_fn);f.detach();}// okay, program continuesFiber
IDs
Objects of class fiber::id can be
used to identify fibers. Each running fiber has a unique fiber::id obtainable
from the corresponding fiber
by calling the fiber::get_id() member
function. Objects of class fiber::id can be
copied, and used as keys in associative containers: the full range of comparison
operators is provided. They can also be written to an output stream using the
stream insertion operator, though the output format is unspecified.
Each instance of fiber::id either
refers to some fiber, or not-a-fiber. Instances that refer
to not-a-fiber compare equal to each other, but not equal
to any instances that refer to an actual fiber. The comparison operators on
fiber::id yield a total order for every non-equal
fiber::id.
Enumeration
launchlaunch specifies whether control
passes immediately into a newly-launched fiber.
enumclasslaunch{dispatch,post};dispatchEffects:
A fiber launched with launch==dispatch
is entered immediately. In other words, launching a fiber with dispatch suspends the caller (the previously-running
fiber) until the fiber scheduler has a chance to resume it later.
postEffects:
A fiber launched with launch==post
is passed to the fiber scheduler as ready, but it is not yet entered.
The caller (the previously-running fiber) continues executing. The newly-launched
fiber will be entered when the fiber scheduler has a chance to resume
it later.
Note:
If launch is not explicitly
specified, post is the
default.
Class
fiber#include<boost/fiber/fiber.hpp>namespaceboost{namespacefibers{classfiber{public:classid;constexprfiber()noexcept;template<typenameFn,typename...Args>fiber(Fn&&,Args&&...);template<typenameFn,typename...Args>fiber(launch,Fn&&,Args&&...);template<typenameStackAllocator,typenameFn,typename...Args>fiber(std::allocator_arg_t,StackAllocator&&,Fn&&,Args&&...);template<typenameStackAllocator,typenameFn,typename...Args>fiber(launch,std::allocator_arg_t,StackAllocator&&,Fn&&,Args&&...);~fiber();fiber(fiberconst&)=delete;fiber&operator=(fiberconst&)=delete;fiber(fiber&&)noexcept;fiber&operator=(fiber&&)noexcept;voidswap(fiber&)noexcept;booljoinable()constnoexcept;idget_id()constnoexcept;voiddetach();voidjoin();template<typenamePROPS>PROPS&properties();};booloperator<(fiberconst&,fiberconst&)noexcept;voidswap(fiber&,fiber&)noexcept;template<typenameSchedAlgo,typename...Args>voiduse_scheduling_algorithm(Args&&...)noexcept;boolhas_ready_fibers()noexcept;}}Default
constructor
constexprfiber()noexcept;Effects:
Constructs a fiber instance that refers to not-a-fiber.
Postconditions:this->get_id()==fiber::id()Throws:
Nothing
Constructor
template<typenameFn,typename...Args>fiber(Fn&&fn,Args&&...args);template<typenameFn,typename...Args>fiber(launchpolicy,Fn&&fn,Args&&...args);template<typenameStackAllocator,typenameFn,typename...Args>fiber(std::allocator_arg_t,StackAllocator&&salloc,Fn&&fn,Args&&...args);template<typenameStackAllocator,typenameFn,typename...Args>fiber(launchpolicy,std::allocator_arg_t,StackAllocator&&salloc,Fn&&fn,Args&&...args);Preconditions:Fn must be copyable
or movable.
Effects:fn is copied or moved
into internal storage for access by the new fiber. If launch is
specified (or defaulted) to post,
the new fiber is marked ready and will be entered at
the next opportunity. If launch
is specified as dispatch,
the calling fiber is suspended and the new fiber is entered immediately.
Postconditions:*this
refers to the newly created fiber of execution.
Throws:fiber_error if an error
occurs.
Note:StackAllocator
is required to allocate a stack for the internal __econtext__. If
StackAllocator is not
explicitly passed, the default stack allocator depends on BOOST_USE_SEGMENTED_STACKS: if defined,
you will get a segmented_stack, else a fixedsize_stack.
See also:std::allocator_arg_t, Stack
allocation
Move
constructor
fiber(fiber&&other)noexcept;Effects:
Transfers ownership of the fiber managed by other
to the newly constructed fiber instance.
Postconditions:other.get_id()==fiber::id() and get_id() returns the value of other.get_id()
prior to the construction
Throws:
Nothing
Move
assignment operator
fiber&operator=(fiber&&other)noexcept;Effects:
Transfers ownership of the fiber managed by other
(if any) to *this.
Postconditions:other->get_id()==fiber::id() and get_id() returns the value of other.get_id()
prior to the assignment.
Throws:
Nothing
Destructor
~fiber();Effects:
If the fiber is fiber::joinable(), calls std::terminate.
Destroys *this.
Note:
The programmer must ensure that the destructor is never executed while
the fiber is still fiber::joinable(). Even if you know
that the fiber has completed, you must still call either fiber::join() or
fiber::detach() before destroying the fiber
object.
Member function joinable()
booljoinable()constnoexcept;Returns:true if *this
refers to a fiber of execution, which may or may not have completed;
otherwise false.
Throws:
Nothing
Member function join()
voidjoin();Preconditions:
the fiber is fiber::joinable().
Effects:
Waits for the referenced fiber of execution to complete.
Postconditions:
The fiber of execution referenced on entry has completed. *this
no longer refers to any fiber of execution.
Throws:fiber_errorError Conditions:resource_deadlock_would_occur: if
this->get_id()==boost::this_fiber::get_id(). invalid_argument:
if the fiber is not fiber::joinable().
Member function detach()
voiddetach();Preconditions:
the fiber is fiber::joinable().
Effects:
The fiber of execution becomes detached, and no longer has an associated
fiber object.
Postconditions:*this
no longer refers to any fiber of execution.
Throws:fiber_errorError Conditions:invalid_argument: if the fiber is
not fiber::joinable().
Member function get_id()
fiber::idget_id()constnoexcept;Returns:
If *this
refers to a fiber of execution, an instance of fiber::id that represents that fiber. Otherwise
returns a default-constructed fiber::id.
Throws:
Nothing
See also:this_fiber::get_id()
Templated member
function properties()
template<typenamePROPS>PROPS&properties();Preconditions:*this
refers to a fiber of execution. use_scheduling_algorithm() has
been called from this thread with a subclass of algorithm_with_properties<> with
the same template argument PROPS.
Returns:
a reference to the scheduler properties instance for *this.
Throws:std::bad_cast if use_scheduling_algorithm() was called with a algorithm_with_properties
subclass with some other template parameter than PROPS.
Note:algorithm_with_properties<> provides
a way for a user-coded scheduler to associate extended properties,
such as priority, with a fiber instance. This method allows access
to those user-provided properties.
See also:
Customization
Member function swap()
voidswap(fiber&other)noexcept;Effects:
Exchanges the fiber of execution associated with *this and other,
so *this
becomes associated with the fiber formerly associated with other, and vice-versa.
Postconditions:this->get_id()
returns the same value as other.get_id() prior to the call. other.get_id()
returns the same value as this->get_id() prior to the call.
Throws:
Nothing
Non-member function
swap()voidswap(fiber&l,fiber&r)noexcept;Effects:
Same as l.swap(r).
Throws:
Nothing
Non-member function operator<()booloperator<(fiberconst&l,fiberconst&r)noexcept;Returns:true if l.get_id()<r.get_id() is true,
false otherwise.
Throws:
Nothing.
Non-member
function use_scheduling_algorithm()template<typenameSchedAlgo,typename...Args>voiduse_scheduling_algorithm(Args&&...args)noexcept;Effects:
Directs Boost.Fiber to use SchedAlgo, which must be a concrete
subclass of algorithm, as the scheduling algorithm for
all fibers in the current thread. Pass any required SchedAlgo
constructor arguments as args.
Note:
If you want a given thread to use a non-default scheduling algorithm,
make that thread call use_scheduling_algorithm() before any other Boost.Fiber
entry point. If no scheduler has been set for the current thread by
the time Boost.Fiber needs to use
it, the library will create a default round_robin instance
for this thread.
Throws:
Nothing
See also:
Scheduling, Customization
Non-member function
has_ready_fibers()boolhas_ready_fibers()noexcept;Returns:true if scheduler has
fibers ready to run.
Throws:
Nothing
Note:
Can be used for work-stealing to find an idle scheduler.
Class fiber::id#include<boost/fiber/fiber.hpp>namespaceboost{namespacefibers{classid{public:constexprid()noexcept;booloperator==(idconst&)constnoexcept;booloperator!=(idconst&)constnoexcept;booloperator<(idconst&)constnoexcept;booloperator>(idconst&)constnoexcept;booloperator<=(idconst&)constnoexcept;booloperator>=(idconst&)constnoexcept;template<typenamecharT,classtraitsT>friendstd::basic_ostream<charT,traitsT>&operator<<(std::basic_ostream<charT,traitsT>&,idconst&);};}}Constructor
constexprid()noexcept;Effects:
Represents an instance of not-a-fiber.
Throws:
Nothing.
Member function
operator==()
booloperator==(idconst&other)constnoexcept;Returns:true if *this
and other represent
the same fiber, or both represent not-a-fiber,
false otherwise.
Throws:
Nothing.
Member
function operator!=()
booloperator!=(idconst&other)constnoexcept;Returns:! (other == * this)Throws:
Nothing.
Member function
operator<()
booloperator<(idconst&other)constnoexcept;Returns:true if *this!=other
is true and the implementation-defined total order of fiber::id values places *this before other,
false otherwise.
Throws:
Nothing.
Member
function operator>()
booloperator>(idconst&other)constnoexcept;Returns:other<*thisThrows:
Nothing.
Member
function operator<=()
booloperator<=(idconst&other)constnoexcept;Returns:!(other<*this)Throws:
Nothing.
Member
function operator>=()
booloperator>=(idconst&other)constnoexcept;Returns:!(*this<other)Throws:
Nothing.
operator<<
template<typenamecharT,classtraitsT>std::basic_ostream<charT,traitsT>&operator<<(std::basic_ostream<charT,traitsT>&os,idconst&other);Efects:
Writes the representation of other
to stream os. The representation
is unspecified.
Returns:osNamespace this_fiber
In general, this_fiber operations
may be called from the main fiber — the fiber on which function
main()
is entered — as well as from an explicitly-launched thread’s thread-function.
That is, in many respects the main fiber on each thread can be treated like
an explicitly-launched fiber.
namespaceboost{namespacethis_fiber{fibers::fiber::idget_id()noexcept;voidyield()noexcept;template<typenameClock,typenameDuration>voidsleep_until(std::chrono::time_point<Clock,Duration>const&);template<typenameRep,typenamePeriod>voidsleep_for(std::chrono::duration<Rep,Period>const&);template<typenamePROPS>PROPS&properties();}}
Non-member
function this_fiber::get_id()#include<boost/fiber/operations.hpp>namespaceboost{namespacefibers{fiber::idget_id()noexcept;}}Returns:
An instance of fiber::id that
represents the currently executing fiber.
Throws:
Nothing.
Non-member
function this_fiber::sleep_until()#include<boost/fiber/operations.hpp>namespaceboost{namespacefibers{template<typenameClock,typenameDuration>voidsleep_until(std::chrono::time_point<Clock,Duration>const&abs_time);}}Effects:
Suspends the current fiber until the time point specified by abs_time has been reached.
Throws:
timeout-related exceptions.
Note:
The current fiber will not resume before abs_time,
but there are no guarantees about how soon after abs_time
it might resume.
Note:timeout-related exceptions are as defined in the C++
Standard, section 30.2.4 Timing specifications
[thread.req.timing]: A function that takes an argument
which specifies a timeout will throw if, during its execution, a clock,
time point, or time duration throws an exception. Such exceptions are
referred to as timeout-related exceptions.
Non-member
function this_fiber::sleep_for()#include<boost/fiber/operations.hpp>namespaceboost{namespacefibers{template<classRep,classPeriod>voidsleep_for(std::chrono::duration<Rep,Period>const&rel_time);}}Effects:
Suspends the current fiber until the time duration specified by rel_time has elapsed.
Throws:
timeout-related exceptions.
Note:
The current fiber will not resume before rel_time
has elapsed, but there are no guarantees about how soon after that
it might resume.
Non-member function
this_fiber::yield()#include<boost/fiber/operations.hpp>namespaceboost{namespacefibers{voidyield()noexcept;}}Effects:
Relinquishes execution control, allowing other fibers to run.
Throws:
Nothing.
Note:
A fiber that calls yield() is not suspended: it is immediately
passed to the scheduler as ready to run.
Non-member
function this_fiber::properties()#include<boost/fiber/operations.hpp>namespaceboost{namespacefibers{template<typenamePROPS>PROPS&properties();}}Preconditions:use_scheduling_algorithm() has been called from
this thread with a subclass of algorithm_with_properties<> with
the same template argument PROPS.
Returns:
a reference to the scheduler properties instance for the currently
running fiber.
Throws:std::bad_cast if use_scheduling_algorithm() was called with an algorithm_with_properties subclass
with some other template parameter than PROPS.
Note:algorithm_with_properties<> provides
a way for a user-coded scheduler to associate extended properties,
such as priority, with a fiber instance. This function allows access
to those user-provided properties.
Note:
The first time this function is called from the main fiber of a thread,
it may internally yield, permitting other fibers to run.
See also:
Customization
Scheduling
The fibers in a thread are coordinated by a fiber manager. Fibers trade control
cooperatively, rather than preemptively: the currently-running fiber retains
control until it invokes some operation that passes control to the manager.
Each time a fiber suspends (or yields), the fiber manager consults a scheduler
to determine which fiber will run next.
Boost.Fiber provides the fiber manager, but
the scheduler is a customization point. (See Customization.)
Each thread has its own scheduler. Different threads in a process may use different
schedulers. By default, Boost.Fiber implicitly
instantiates round_robin as the scheduler for each thread.
You are explicitly permitted to code your own algorithm subclass.
For the most part, your algorithm
subclass need not defend against cross-thread calls: the fiber manager intercepts
and defers such calls. Most algorithm
methods are only ever directly called from the thread whose fibers it is managing
— with exceptions as documented below.
Your algorithm subclass is
engaged on a particular thread by calling use_scheduling_algorithm():
voidthread_fn(){boost::fibers::use_scheduling_algorithm<my_fiber_scheduler>();...}
A scheduler class must implement interface algorithm. Boost.Fiber provides schedulers: round_robin,
work_stealing, numa::work_stealing and
shared_work.
voidthread(std::uint32_tthread_count){// thread registers itself at work-stealing schedulerboost::fibers::use_scheduling_algorithm<boost::fibers::algo::work_stealing>(thread_count);...}// count of logical cpusstd::uint32_tthread_count=std::thread::hardware_concurrency();// start worker-threads firststd::vector<std::thread>threads;for(std::uint32_ti=1/* count start-thread */;i<thread_count;++i){// spawn threadthreads.emplace_back(thread,thread_count);}// start-thread registers itself at work-stealing schedulerboost::fibers::use_scheduling_algorithm<boost::fibers::algo::work_stealing>(thread_count);...
The example spawns as many threads as std::thread::hardware_concurrency() returns. Each thread runs a work_stealing scheduler.
Each instance of this scheduler needs to know how many threads run the work-stealing
scheduler in the program. If the local queue of one thread runs out of ready
fibers, the thread tries to steal a ready fiber from another thread running
this scheduler.
Class algorithmalgorithm is the abstract base
class defining the interface that a fiber scheduler must implement.
#include<boost/fiber/algo/algorithm.hpp>namespaceboost{namespacefibers{namespacealgo{structalgorithm{virtual~algorithm();virtualvoidawakened(context*)noexcept=0;virtualcontext*pick_next()noexcept=0;virtualboolhas_ready_fibers()constnoexcept=0;virtualvoidsuspend_until(std::chrono::steady_clock::time_pointconst&)noexcept=0;virtualvoidnotify()noexcept=0;};}}}
Member function
awakened()
virtualvoidawakened(context*f)noexcept=0;Effects:
Informs the scheduler that fiber f
is ready to run. Fiber f
might be newly launched, or it might have been blocked but has just been
awakened, or it might have called this_fiber::yield().
Note:
This method advises the scheduler to add fiber f
to its collection of fibers ready to run. A typical scheduler implementation
places f into a queue.
See also:round_robin
Member
function pick_next()
virtualcontext*pick_next()noexcept=0;Returns:
the fiber which is to be resumed next, or nullptr
if there is no ready fiber.
Note:
This is where the scheduler actually specifies the fiber which is to
run next. A typical scheduler implementation chooses the head of the
ready queue.
See also:round_robin
Member
function has_ready_fibers()
virtualboolhas_ready_fibers()constnoexcept=0;Returns:true if scheduler has fibers
ready to run.
Member
function suspend_until()
virtualvoidsuspend_until(std::chrono::steady_clock::time_pointconst&abs_time)noexcept=0;Effects:
Informs the scheduler that no fiber will be ready until time-point abs_time.
Note:
This method allows a custom scheduler to yield control to the containing
environment in whatever way makes sense. The fiber manager is stating
that suspend_until()
need not return until abs_time
— or algorithm::notify() is called — whichever comes first.
The interaction with notify() means that, for instance, calling
std::this_thread::sleep_until(abs_time)
would be too simplistic. round_robin::suspend_until() uses
a std::condition_variable to coordinate
with round_robin::notify().
Note:
Given that notify()
might be called from another thread, your suspend_until() implementation — like the rest of your
algorithm implementation
— must guard any data it shares with your notify() implementation.
Member function
notify()
virtualvoidnotify()noexcept=0;Effects:
Requests the scheduler to return from a pending call to algorithm::suspend_until().
Note:
Alone among the algorithm
methods, notify()
may be called from another thread. Your notify() implementation must guard any data
it shares with the rest of your algorithm
implementation.
Class round_robin
This class implements algorithm, scheduling fibers in round-robin
fashion.
#include<boost/fiber/algo/round_robin.hpp>namespaceboost{namespacefibers{namespacealgo{classround_robin:publicalgorithm{virtualvoidawakened(context*)noexcept;virtualcontext*pick_next()noexcept;virtualboolhas_ready_fibers()constnoexcept;virtualvoidsuspend_until(std::chrono::steady_clock::time_pointconst&)noexcept;virtualvoidnotify()noexcept;};}}}
Member
function awakened()
virtualvoidawakened(context*f)noexcept;Effects:
Enqueues fiber f onto
a ready queue.
Throws:
Nothing.
Member
function pick_next()
virtualcontext*pick_next()noexcept;Returns:
the fiber at the head of the ready queue, or nullptr
if the queue is empty.
Throws:
Nothing.
Note:
Placing ready fibers onto the tail of a queue, and returning them from
the head of that queue, shares the thread between ready fibers in round-robin
fashion.
Member
function has_ready_fibers()
virtualboolhas_ready_fibers()constnoexcept;Returns:true if scheduler has fibers
ready to run.
Throws:
Nothing.
Member
function suspend_until()
virtualvoidsuspend_until(std::chrono::steady_clock::time_pointconst&abs_time)noexcept;Effects:
Informs round_robin that
no ready fiber will be available until time-point abs_time.
This implementation blocks in std::condition_variable::wait_until().
Throws:
Nothing.
Member function
notify()
virtualvoidnotify()noexcept=0;Effects:
Wake up a pending call to round_robin::suspend_until(),
some fibers might be ready. This implementation wakes suspend_until() via std::condition_variable::notify_all().
Throws:
Nothing.
Class work_stealing
This class implements algorithm; if the local ready-queue runs
out of ready fibers, ready fibers are stolen from other schedulers. The
victim scheduler (from which a ready fiber is stolen) is selected at random.
Worker-threads are stored in a static variable, dynamically adding/removing
worker threads is not supported.
#include<boost/fiber/algo/work_stealing.hpp>namespaceboost{namespacefibers{namespacealgo{classwork_stealing:publicalgorithm{public:work_stealing(std::uint32_tthread_count,boolsuspend=false);work_stealing(work_stealingconst&)=delete;work_stealing(work_stealing&&)=delete;work_stealing&operator=(work_stealingconst&)=delete;work_stealing&operator=(work_stealing&&)=delete;virtualvoidawakened(context*)noexcept;virtualcontext*pick_next()noexcept;virtualboolhas_ready_fibers()constnoexcept;virtualvoidsuspend_until(std::chrono::steady_clock::time_pointconst&)noexcept;virtualvoidnotify()noexcept;};}}}Constructor
work_stealing(std::uint32_tthread_count,boolsuspend=false);Effects:
Constructs work-stealing scheduling algorithm. thread_count
represents the number of threads running this algorithm.
Throws:system_errorNote:
If suspend is set to
true, then the scheduler
suspends if no ready fiber could be stolen. The scheduler will by woken
up if a sleeping fiber times out or it was notified from remote (other
thread or fiber scheduler).
Member
function awakened()
virtualvoidawakened(context*f)noexcept;Effects:
Enqueues fiber f onto
the shared ready queue.
Throws:
Nothing.
Member
function pick_next()
virtualcontext*pick_next()noexcept;Returns:
the fiber at the head of the ready queue, or nullptr
if the queue is empty.
Throws:
Nothing.
Note:
Placing ready fibers onto the tail of the sahred queue, and returning
them from the head of that queue, shares the thread between ready fibers
in round-robin fashion.
Member
function has_ready_fibers()
virtualboolhas_ready_fibers()constnoexcept;Returns:true if scheduler has fibers
ready to run.
Throws:
Nothing.
Member
function suspend_until()
virtualvoidsuspend_until(std::chrono::steady_clock::time_pointconst&abs_time)noexcept;Effects:
Informs work_stealing
that no ready fiber will be available until time-point abs_time. This implementation blocks
in std::condition_variable::wait_until().
Throws:
Nothing.
Member
function notify()
virtualvoidnotify()noexcept=0;Effects:
Wake up a pending call to work_stealing::suspend_until(),
some fibers might be ready. This implementation wakes suspend_until() via std::condition_variable::notify_all().
Throws:
Nothing.
Class shared_work
Because of the non-locality of data, shared_work is
less performant than work_stealing.
This class implements algorithm, scheduling fibers in round-robin
fashion. Ready fibers are shared between all instances (running on different
threads) of shared_work, thus the work is distributed equally over all threads.
Worker-threads are stored in a static variable, dynamically adding/removing
worker threads is not supported.
#include<boost/fiber/algo/shared_work.hpp>namespaceboost{namespacefibers{namespacealgo{classshared_work:publicalgorithm{virtualvoidawakened(context*)noexcept;virtualcontext*pick_next()noexcept;virtualboolhas_ready_fibers()constnoexcept;virtualvoidsuspend_until(std::chrono::steady_clock::time_pointconst&)noexcept;virtualvoidnotify()noexcept;};}}}
Member
function awakened()
virtualvoidawakened(context*f)noexcept;Effects:
Enqueues fiber f onto
the shared ready queue.
Throws:
Nothing.
Member
function pick_next()
virtualcontext*pick_next()noexcept;Returns:
the fiber at the head of the ready queue, or nullptr
if the queue is empty.
Throws:
Nothing.
Note:
Placing ready fibers onto the tail of the shared queue, and returning
them from the head of that queue, shares the thread between ready fibers
in round-robin fashion.
Member
function has_ready_fibers()
virtualboolhas_ready_fibers()constnoexcept;Returns:true if scheduler has fibers
ready to run.
Throws:
Nothing.
Member
function suspend_until()
virtualvoidsuspend_until(std::chrono::steady_clock::time_pointconst&abs_time)noexcept;Effects:
Informs shared_work that
no ready fiber will be available until time-point abs_time.
This implementation blocks in std::condition_variable::wait_until().
Throws:
Nothing.
Member function
notify()
virtualvoidnotify()noexcept=0;Effects:
Wake up a pending call to shared_work::suspend_until(),
some fibers might be ready. This implementation wakes suspend_until() via std::condition_variable::notify_all().
Throws:
Nothing.
Custom
Scheduler Fiber Properties
A scheduler class directly derived from algorithm can use any
information available from context to implement the algorithm interface. But a custom scheduler
might need to track additional properties for a fiber. For instance, a priority-based
scheduler would need to track a fiber’s priority.
Boost.Fiber provides a mechanism by which
your custom scheduler can associate custom properties with each fiber.
Class
fiber_properties
A custom fiber properties class must be derived from fiber_properties.
#include<boost/fiber/properties.hpp>namespaceboost{namespacefibers{classfiber_properties{public:fiber_properties(context*)noexcept;virtual~fiber_properties();protected:voidnotify()noexcept;};}}Constructor
fiber_properties(context*f)noexcept;Effects:
Constructs base-class component of custom subclass.
Throws:
Nothing.
Note:
Your subclass constructor must accept a context* and pass it to the base-class fiber_properties constructor.
Member
function notify()
voidnotify()noexcept;Effects:
Pass control to the custom algorithm_with_properties<> subclass’s
algorithm_with_properties::property_change() method.
Throws:
Nothing.
Note:
A custom scheduler’s algorithm_with_properties::pick_next() method
might dynamically select from the ready fibers, or algorithm_with_properties::awakened() might
instead insert each ready fiber into some form of ready queue for pick_next().
In the latter case, if application code modifies a fiber property (e.g.
priority) that should affect that fiber’s relationship to other ready
fibers, the custom scheduler must be given the opportunity to reorder
its ready queue. The custom property subclass should implement an access
method to modify such a property; that access method should call notify()
once the new property value has been stored. This passes control to the
custom scheduler’s property_change() method, allowing the custom scheduler
to reorder its ready queue appropriately. Use at your discretion. Of
course, if you define a property which does not affect the behavior of
the pick_next()
method, you need not call notify() when that property is modified.
Template
algorithm_with_properties<>
A custom scheduler that depends on a custom properties class PROPS should be derived from algorithm_with_properties<PROPS>.
PROPS should be derived from
fiber_properties.
#include<boost/fiber/algorithm.hpp>namespaceboost{namespacefibers{namespacealgo{template<typenamePROPS>structalgorithm_with_properties{virtualvoidawakened(context*,PROPS&)noexcept=0;virtualcontext*pick_next()noexcept;virtualboolhas_ready_fibers()constnoexcept;virtualvoidsuspend_until(std::chrono::steady_clock::time_pointconst&)noexcept=0;virtualvoidnotify()noexcept=0;PROPS&properties(context*)noexcept;virtualvoidproperty_change(context*,PROPS&)noexcept;virtualfiber_properties*new_properties(context*);};}}}
Member
function awakened()
virtualvoidawakened(context*f,PROPS&properties)noexcept;Effects:
Informs the scheduler that fiber f
is ready to run, like algorithm::awakened(). Passes
the fiber’s associated PROPS
instance.
Throws:
Nothing.
Note:
An algorithm_with_properties<> subclass must override this method
instead of algorithm::awakened().
Member
function pick_next()
virtualcontext*pick_next()noexcept;Returns:
the fiber which is to be resumed next, or nullptr
if there is no ready fiber.
Throws:
Nothing.
Note:
same as algorithm::pick_next()
Member
function has_ready_fibers()
virtualboolhas_ready_fibers()constnoexcept;Returns:true if scheduler has fibers
ready to run.
Throws:
Nothing.
Note:
same as algorithm::has_ready_fibers()
Member
function suspend_until()
virtualvoidsuspend_until(std::chrono::steady_clock::time_pointconst&abs_time)noexcept=0;Effects:
Informs the scheduler that no fiber will be ready until time-point abs_time.
Note:
same as algorithm::suspend_until()
Member
function notify()
virtualvoidnotify()noexcept=0;Effects:
Requests the scheduler to return from a pending call to algorithm_with_properties::suspend_until().
Note:
same as algorithm::notify()
Member
function properties()
PROPS&properties(context*f)noexcept;Returns:
the PROPS instance associated
with fiber f.
Throws:
Nothing.
Note:
The fiber’s associated PROPS
instance is already passed to algorithm_with_properties::awakened() and
algorithm_with_properties::property_change().
However, every algorithm subclass is expected to track
a collection of ready context instances. This method allows
your custom scheduler to retrieve the fiber_properties subclass
instance for any context
in its collection.
Member
function property_change()
virtualvoidproperty_change(context*f,PROPS&properties)noexcept;Effects:
Notify the custom scheduler of a possibly-relevant change to a property
belonging to fiber f.
properties contains the
new values of all relevant properties.
Throws:
Nothing.
Note:
This method is only called when a custom fiber_properties subclass
explicitly calls fiber_properties::notify().
Member
function new_properties()
virtualfiber_properties*new_properties(context*f);Returns:
A new instance of fiber_properties subclass PROPS.
Note:
By default, algorithm_with_properties<>::new_properties() simply returns newPROPS(f),
placing the PROPS instance
on the heap. Override this method to allocate PROPS
some other way. The returned fiber_properties
pointer must point to the PROPS
instance to be associated with fiber f.
Class
context
While you are free to treat context* as an opaque token, certain context members may be useful to a custom
scheduler implementation.
Of particular note is the fact that context contains a hook to participate in
a boost::intrusive::listtypedef’ed as boost::fibers::scheduler::ready_queue_t.
This hook is reserved for use by algorithm implementations. (For
instance, round_robin contains a ready_queue_t
instance to manage its ready fibers.) See context::ready_is_linked(),
context::ready_link(), context::ready_unlink().
Your algorithm implementation
may use any container you desire to manage passed context
instances. ready_queue_t avoids
some of the overhead of typical STL containers.
#include<boost/fiber/context.hpp>namespaceboost{namespacefibers{enumclasstype{none=unspecified,main_context=unspecified,// fiber associated with thread's stackdispatcher_context=unspecified,// special fiber for maintenance operationsworker_context=unspecified,// fiber not special to the librarypinned_context=unspecified// fiber must not be migrated to another thread};classcontext{public:classid;staticcontext*active()noexcept;context(contextconst&)=delete;context&operator=(contextconst&)=delete;idget_id()constnoexcept;voiddetach()noexcept;voidattach(context*)noexcept;boolis_context(type)constnoexcept;boolis_terminated()constnoexcept;boolready_is_linked()constnoexcept;boolremote_ready_is_linked()constnoexcept;boolwait_is_linked()constnoexcept;template<typenameList>voidready_link(List&)noexcept;template<typenameList>voidremote_ready_link(List&)noexcept;template<typenameList>voidwait_link(List&)noexcept;voidready_unlink()noexcept;voidremote_ready_unlink()noexcept;voidwait_unlink()noexcept;voidsuspend()noexcept;voidschedule(context*)noexcept;};booloperator<(contextconst&l,contextconst&r)noexcept;}}
Static member function
active()
staticcontext*active()noexcept;Returns:
Pointer to instance of current fiber.
Throws:
Nothing
Member function get_id()
context::idget_id()constnoexcept;Returns:
If *this
refers to a fiber of execution, an instance of fiber::id that represents that fiber. Otherwise
returns a default-constructed fiber::id.
Throws:
Nothing
See also:fiber::get_id()
Member function attach()
voidattach(context*f)noexcept;Precondition:this->get_scheduler()==nullptrEffects:
Attach fiber f to scheduler
running *this.
Postcondition:this->get_scheduler()!=nullptrThrows:
Nothing
Note:
A typical call: boost::fibers::context::active()->attach(f);Note:f must not be the running
fiber’s context. It must not be blocked
or terminated. It must not be a pinned_context.
It must be currently detached. It must not currently be linked into an
algorithm implementation’s ready queue. Most of these conditions
are implied by f being
owned by an algorithm
implementation: that is, it has been passed to algorithm::awakened() but
has not yet been returned by algorithm::pick_next().
Typically a pick_next() implementation would call attach()
with the context*
it is about to return. It must first remove f
from its ready queue. You should never pass a pinned_context
to attach()
because you should never have called its detach() method in the first place.
Member function detach()
voiddetach()noexcept;Precondition:(this->get_scheduler()!=nullptr)&&!this->is_context(pinned_context)Effects:
Detach fiber *this
from its scheduler running *this.
Throws:
Nothing
Postcondition:this->get_scheduler()==nullptrNote:
This method must be called on the thread with which the fiber is currently
associated. *this
must not be the running fiber’s context. It must not be blocked
or terminated. It must not be a pinned_context.
It must not be detached already. It must not already be linked into an
algorithm implementation’s ready queue. Most of these conditions
are implied by *this
being passed to algorithm::awakened(); an awakened()
implementation must, however, test for pinned_context.
It must call detach()before linking *this into its ready queue.
Note:
In particular, it is erroneous to attempt to migrate a fiber from one
thread to another by calling both detach() and attach() in the algorithm::pick_next() method.
pick_next()
is called on the intended destination thread. detach() must be called on the fiber’s original
thread. You must call detach() in the corresponding awakened()
method.
Note:
Unless you intend make a fiber available for potential migration to a
different thread, you should call neither detach() nor attach() with its context.
Member function
is_context()
boolis_context(typet)constnoexcept;Returns:true if *this is of the specified type.
Throws:
Nothing
Note:type::worker_context here means any fiber
not special to the library. For type::main_context
the context is associated
with the main fiber of the thread: the one implicitly
created by the thread itself, rather than one explicitly created by
Boost.Fiber. For type::dispatcher_context
the context is associated
with a dispatching fiber, responsible for dispatching
awakened fibers to a scheduler’s ready-queue. The dispatching
fiber is an implementation detail of the fiber manager. The context of
the main or dispatching fiber — any fiber
for which is_context(pinned_context) is true
— must never be passed to context::detach().
Member
function is_terminated()
boolis_terminated()constnoexcept;Returns:true if *this is no longer a valid context.
Throws:
Nothing
Note:
The context has returned
from its fiber-function and is no longer considered a valid context.
Member
function ready_is_linked()
boolready_is_linked()constnoexcept;Returns:true if *this is stored in an algorithm
implementation’s
ready-queue.
Throws:
Nothing
Note:
Specifically, this method indicates whether context::ready_link() has
been called on *this.
ready_is_linked()
has no information about participation in any other containers.
Member
function remote_ready_is_linked()
boolremote_ready_is_linked()constnoexcept;Returns:true if *this is stored in the fiber manager’s remote-ready-queue.
Throws:
Nothing
Note:
A context signaled as
ready by another thread is first stored in the fiber manager’s remote-ready-queue.
This is the mechanism by which the fiber manager protects an algorithm implementation
from cross-thread algorithm::awakened() calls.
Member
function wait_is_linked()
boolwait_is_linked()constnoexcept;Returns:true if *this is stored in the wait-queue of some
synchronization object.
Throws:
Nothing
Note:
The context of a fiber
waiting on a synchronization object (e.g. mutex,
condition_variable etc.)
is stored in the wait-queue of that synchronization object.
Member function
ready_link()
template<typenameList>voidready_link(List&lst)noexcept;Effects:
Stores *this
in ready-queue lst.
Throws:
Nothing
Note:
Argument lst must be
a doubly-linked list from Boost.Intrusive,
e.g. an instance of boost::fibers::scheduler::ready_queue_t.
Specifically, it must be a boost::intrusive::list compatible with the list_member_hook stored in the context object.
Member
function remote_ready_link()
template<typenameList>voidremote_ready_link(List&lst)noexcept;Effects:
Stores *this
in remote-ready-queue lst.
Throws:
Nothing
Note:
Argument lst must be
a doubly-linked list from Boost.Intrusive.
Member function
wait_link()
template<typenameList>voidwait_link(List&lst)noexcept;Effects:
Stores *this
in wait-queue lst.
Throws:
Nothing
Note:
Argument lst must be
a doubly-linked list from Boost.Intrusive.
Member
function ready_unlink()
voidready_unlink()noexcept;Effects:
Removes *this
from ready-queue: undoes the effect of context::ready_link().
Throws:
Nothing
Member
function remote_ready_unlink()
voidremote_ready_unlink()noexcept;Effects:
Removes *this
from remote-ready-queue.
Throws:
Nothing
Member
function wait_unlink()
voidwait_unlink()noexcept;Effects:
Removes *this
from wait-queue.
Throws:
Nothing
Member function suspend()
voidsuspend()noexcept;Effects:
Suspends the running fiber (the fiber associated with *this) until some other fiber passes this to context::schedule().
*this
is marked as not-ready, and control passes to the scheduler to select
another fiber to run.
Throws:
Nothing
Note:
This is a low-level API potentially useful for integration with other
frameworks. It is not intended to be directly invoked by a typical application
program.
Note:
The burden is on the caller to arrange for a call to schedule() with a pointer to this
at some future time.
Member function
schedule()
voidschedule(context*ctx)noexcept;Effects:
Mark the fiber associated with context *ctx as being ready to run. This does
not immediately resume that fiber; rather it passes the fiber to the
scheduler for subsequent resumption. If the scheduler is idle (has not
returned from a call to algorithm::suspend_until()),
algorithm::notify() is called to wake it up.
Throws:
Nothing
Note:
This is a low-level API potentially useful for integration with other
frameworks. It is not intended to be directly invoked by a typical application
program.
Note:
It is explicitly supported to call schedule(ctx) from a thread other than the one on
which *ctx
is currently suspended. The corresponding fiber will be resumed on its
original thread in due course.
Non-member function operator<()booloperator<(contextconst&l,contextconst&r)noexcept;Returns:true if l.get_id()<r.get_id()
is true, false
otherwise.
Throws:
Nothing.
Stack allocation
A fiber uses internally an __econtext__ which manages a set of registers
and a stack. The memory used by the stack is allocated/deallocated via a stack_allocator
which is required to model a stack-allocator
concept.
A stack_allocator can be passed to fiber::fiber() or to fibers::async().
stack-allocator
concept
A stack_allocator must satisfy the stack-allocator
concept requirements shown in the following table, in which a is an object of a stack_allocator
type, sctx is a stack_context, and size is a std::size_t:
expression
return type
notes
a(size)
creates a stack allocator
a.allocate()stack_context
creates a stack
a.deallocate(sctx)void
deallocates the stack created by a.allocate()
The implementation of allocate() might include logic to protect against
exceeding the context's available stack size rather than leaving it as undefined
behaviour.
Calling deallocate()
with a stack_context not obtained from
allocate()
results in undefined behaviour.
The memory for the stack is not required to be aligned; alignment takes place
inside __econtext__.
See also Boost.Context
stack allocation. In particular, traits_type
methods are as described for boost::context::stack_traits.
Class
protected_fixedsize_stackBoost.Fiber provides the class protected_fixedsize_stack which
models the stack-allocator
concept. It appends a guard page at the end of each stack
to protect against exceeding the stack. If the guard page is accessed (read
or write operation) a segmentation fault/access violation is generated by the
operating system.
Using protected_fixedsize_stack is expensive.
Launching a new fiber with a stack of this type incurs the overhead of setting
the memory protection; once allocated, this stack is just as efficient to
use as fixedsize_stack.
The appended guardpage
is not mapped to physical memory, only virtual
addresses are used.
#include<boost/fiber/protected_fixedsize.hpp>namespaceboost{namespacefibers{structprotected_fixedsize{protected_fixesize(std::size_tsize=traits_type::default_size());stack_contextallocate();voiddeallocate(stack_context&);}}}
Member
function allocate()
stack_contextallocate();Preconditions:traits_type::minimum_size()<=size
and traits_type::is_unbounded()||(size<=traits_type::maximum_size()).
Effects:
Allocates memory of at least size
bytes and stores a pointer to the stack and its actual size in sctx. Depending on the architecture
(the stack grows downwards/upwards) the stored address is the highest/lowest
address of the stack.
Member
function deallocate()
voiddeallocate(stack_context&sctx);Preconditions:sctx.sp is valid, traits_type::minimum_size()<=sctx.size and traits_type::is_unbounded()||(sctx.size<=traits_type::maximum_size()).
Effects:
Deallocates the stack space.
Class
pooled_fixedsize_stackBoost.Fiber provides the class pooled_fixedsize_stack which
models the stack-allocator
concept. In contrast to protected_fixedsize_stack it
does not append a guard page at the end of each stack. The memory is managed
internally by boost::pool<>.
#include<boost/fiber/pooled_fixedsize_stack.hpp>namespaceboost{namespacefibers{structpooled_fixedsize_stack{pooled_fixedsize_stack(std::size_tstack_size=traits_type::default_size(),std::size_tnext_size=32,std::size_tmax_size=0);stack_contextallocate();voiddeallocate(stack_context&);}}}
Constructor
pooled_fixedsize_stack(std::size_tstack_size,std::size_tnext_size,std::size_tmax_size);Preconditions:traits_type::is_unbounded()||(traits_type::maximum_size()>=stack_size) and 0<next_size.
Effects:
Allocates memory of at least stack_size
bytes and stores a pointer to the stack and its actual size in sctx. Depending on the architecture
(the stack grows downwards/upwards) the stored address is the highest/lowest
address of the stack. Argument next_size
determines the number of stacks to request from the system the first
time that *this
needs to allocate system memory. The third argument max_size
controls how much memory might be allocated for stacks — a value of zero
means no upper limit.
Member
function allocate()
stack_contextallocate();Preconditions:traits_type::is_unbounded()||(traits_type::maximum_size()>=stack_size).
Effects:
Allocates memory of at least stack_size
bytes and stores a pointer to the stack and its actual size in sctx. Depending on the architecture
(the stack grows downwards/upwards) the stored address is the highest/lowest
address of the stack.
Member
function deallocate()
voiddeallocate(stack_context&sctx);Preconditions:sctx.sp is valid, traits_type::is_unbounded()||(traits_type::maximum_size()>=sctx.size).
Effects:
Deallocates the stack space.
This stack allocator is not thread safe.
Class
fixedsize_stackBoost.Fiber provides the class fixedsize_stack which
models the stack-allocator
concept. In contrast to protected_fixedsize_stack it
does not append a guard page at the end of each stack. The memory is simply
managed by std::malloc()
and std::free().
#include<boost/context/fixedsize_stack.hpp>namespaceboost{namespacefibers{structfixedsize_stack{fixedsize_stack(std::size_tsize=traits_type::default_size());stack_contextallocate();voiddeallocate(stack_context&);}}}
Member function
allocate()
stack_contextallocate();Preconditions:traits_type::minimum_size()<=size
and traits_type::is_unbounded()||(traits_type::maximum_size()>=size).
Effects:
Allocates memory of at least size
bytes and stores a pointer to the stack and its actual size in sctx. Depending on the architecture
(the stack grows downwards/upwards) the stored address is the highest/lowest
address of the stack.
Member
function deallocate()
voiddeallocate(stack_context&sctx);Preconditions:sctx.sp is valid, traits_type::minimum_size()<=sctx.size and traits_type::is_unbounded()||(traits_type::maximum_size()>=sctx.size).
Effects:
Deallocates the stack space.
Class
segmented_stackBoost.Fiber supports usage of a segmented_stack,
i.e. the stack grows on demand. The fiber is created with a minimal stack size
which will be increased as required. Class segmented_stack models
the stack-allocator concept.
In contrast to protected_fixedsize_stack and
fixedsize_stack it creates a stack which grows on demand.
Segmented stacks are currently only supported by gcc
from version 4.7 and clang
from version 3.4 onwards. In order to use
a segmented_stackBoost.Fiber
must be built with property segmented-stacks,
e.g. toolset=gcc segmented-stacks=on and
applying BOOST_USE_SEGMENTED_STACKS at b2/bjam command line.
Segmented stacks can only be used with callcc() using property context-impl=ucontext.
#include<boost/fiber/segmented_stack.hpp>namespaceboost{namespacefibers{structsegmented_stack{segmented_stack(std::size_tstack_size=traits_type::default_size());stack_contextallocate();voiddeallocate(stack_context&);}}}
Member function
allocate()
stack_contextallocate();Preconditions:traits_type::minimum_size()<=size
and traits_type::is_unbounded()||(traits_type::maximum_size()>=size).
Effects:
Allocates memory of at least size
bytes and stores a pointer to the stack and its actual size in sctx. Depending on the architecture
(the stack grows downwards/upwards) the stored address is the highest/lowest
address of the stack.
Member
function deallocate()
voiddeallocate(stack_context&sctx);Preconditions:sctx.sp is valid, traits_type::minimum_size()<=sctx.size and traits_type::is_unbounded()||(traits_type::maximum_size()>=sctx.size).
Effects:
Deallocates the stack space.
If the library is compiled for segmented stacks, segmented_stack is
the only available stack allocator.
Support for valgrind
Running programs that switch stacks under valgrind causes problems. Property
(b2 command-line) valgrind=on let
valgrind treat the memory regions as stack space which suppresses the errors.
Synchronization
In general, Boost.Fiber 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.
The fiber synchronization objects provided by this library will, by default,
safely synchronize fibers running on different threads. However, this level
of synchronization can be removed (for performance) by building the library
with BOOST_FIBERS_NO_ATOMICS
defined. When the library is built with that macro, you must ensure that all
the fibers referencing a particular synchronization object are running in the
same thread.
Mutex Types
Class mutex#include<boost/fiber/mutex.hpp>namespaceboost{namespacefibers{classmutex{public:mutex();~mutex();mutex(mutexconst&other)=delete;mutex&operator=(mutexconst&other)=delete;voidlock();booltry_lock();voidunlock();};}}mutex provides an exclusive-ownership mutex. At most one fiber
can own the lock on a given instance of mutex at any time. Multiple
concurrent calls to lock(), try_lock() and unlock() shall be permitted.
Any fiber blocked in lock() is suspended until the owning fiber releases
the lock by calling unlock().
Member function lock()
voidlock();Precondition:
The calling fiber doesn't own the mutex.
Effects:
The current fiber blocks until ownership can be obtained.
Throws:lock_errorError Conditions:resource_deadlock_would_occur: if
boost::this_fiber::get_id()
already owns the mutex.
Member function try_lock()
booltry_lock();Precondition:
The calling fiber doesn't own the mutex.
Effects:
Attempt to obtain ownership for the current fiber without blocking.
Returns:true if ownership was
obtained for the current fiber, false
otherwise.
Throws:lock_errorError Conditions:resource_deadlock_would_occur: if
boost::this_fiber::get_id()
already owns the mutex.
Member function unlock()
voidunlock();Precondition:
The current fiber owns *this.
Effects:
Releases a lock on *this
by the current fiber.
Throws:lock_errorError Conditions:operation_not_permitted: if boost::this_fiber::get_id()
does not own the mutex.
Class timed_mutex#include<boost/fiber/timed_mutex.hpp>namespaceboost{namespacefibers{classtimed_mutex{public:timed_mutex();~timed_mutex();timed_mutex(timed_mutexconst&other)=delete;timed_mutex&operator=(timed_mutexconst&other)=delete;voidlock();booltry_lock();voidunlock();template<typenameClock,typenameDuration>booltry_lock_until(std::chrono::time_point<Clock,Duration>const&timeout_time);template<typenameRep,typenamePeriod>booltry_lock_for(std::chrono::duration<Rep,Period>const&timeout_duration);};}}timed_mutex provides an exclusive-ownership mutex. At most
one fiber can own the lock on a given instance of timed_mutex at
any time. Multiple concurrent calls to lock(), try_lock(), try_lock_until(), try_lock_for() and unlock() shall be permitted.
Member function
lock()
voidlock();Precondition:
The calling fiber doesn't own the mutex.
Effects:
The current fiber blocks until ownership can be obtained.
Throws:lock_errorError Conditions:resource_deadlock_would_occur: if
boost::this_fiber::get_id()
already owns the mutex.
Member
function try_lock()
booltry_lock();Precondition:
The calling fiber doesn't own the mutex.
Effects:
Attempt to obtain ownership for the current fiber without blocking.
Returns:true if ownership was
obtained for the current fiber, false
otherwise.
Throws:lock_errorError Conditions:resource_deadlock_would_occur: if
boost::this_fiber::get_id()
already owns the mutex.
Member function
unlock()
voidunlock();Precondition:
The current fiber owns *this.
Effects:
Releases a lock on *this
by the current fiber.
Throws:lock_errorError Conditions:operation_not_permitted: if boost::this_fiber::get_id()
does not own the mutex.
Templated
member function try_lock_until()
template<typenameClock,typenameDuration>booltry_lock_until(std::chrono::time_point<Clock,Duration>const&timeout_time);Precondition:
The calling fiber doesn't own the mutex.
Effects:
Attempt to obtain ownership for the current fiber. Blocks until ownership
can be obtained, or the specified time is reached. If the specified
time has already passed, behaves as timed_mutex::try_lock().
Returns:true if ownership was
obtained for the current fiber, false
otherwise.
Throws:lock_error, timeout-related
exceptions.
Error Conditions:resource_deadlock_would_occur: if
boost::this_fiber::get_id()
already owns the mutex.
Templated
member function try_lock_for()
template<typenameRep,typenamePeriod>booltry_lock_for(std::chrono::duration<Rep,Period>const&timeout_duration);Precondition:
The calling fiber doesn't own the mutex.
Effects:
Attempt to obtain ownership for the current fiber. Blocks until ownership
can be obtained, or the specified time is reached. If the specified
time has already passed, behaves as timed_mutex::try_lock().
Returns:true if ownership was
obtained for the current fiber, false
otherwise.
Throws:lock_error, timeout-related
exceptions.
Error Conditions:resource_deadlock_would_occur: if
boost::this_fiber::get_id()
already owns the mutex.
Class
recursive_mutex#include<boost/fiber/recursive_mutex.hpp>namespaceboost{namespacefibers{classrecursive_mutex{public:recursive_mutex();~recursive_mutex();recursive_mutex(recursive_mutexconst&other)=delete;recursive_mutex&operator=(recursive_mutexconst&other)=delete;voidlock();booltry_lock()noexcept;voidunlock();};}}recursive_mutex provides an exclusive-ownership recursive
mutex. At most one fiber can own the lock on a given instance of recursive_mutex at
any time. Multiple concurrent calls to lock(), try_lock() and unlock() shall be permitted. A fiber that already
has exclusive ownership of a given recursive_mutex instance
can call lock()
or try_lock()
to acquire an additional level of ownership of the mutex. unlock() must be called once for each level of ownership
acquired by a single fiber before ownership can be acquired by another fiber.
Member
function lock()
voidlock();Effects:
The current fiber blocks until ownership can be obtained.
Throws:
Nothing
Member
function try_lock()
booltry_lock()noexcept;Effects:
Attempt to obtain ownership for the current fiber without blocking.
Returns:true if ownership was
obtained for the current fiber, false
otherwise.
Throws:
Nothing.
Member
function unlock()
voidunlock();Effects:
Releases a lock on *this
by the current fiber.
Throws:lock_errorError Conditions:operation_not_permitted: if boost::this_fiber::get_id()
does not own the mutex.
Class
recursive_timed_mutex#include<boost/fiber/recursive_timed_mutex.hpp>namespaceboost{namespacefibers{classrecursive_timed_mutex{public:recursive_timed_mutex();~recursive_timed_mutex();recursive_timed_mutex(recursive_timed_mutexconst&other)=delete;recursive_timed_mutex&operator=(recursive_timed_mutexconst&other)=delete;voidlock();booltry_lock()noexcept;voidunlock();template<typenameClock,typenameDuration>booltry_lock_until(std::chrono::time_point<Clock,Duration>const&timeout_time);template<typenameRep,typenamePeriod>booltry_lock_for(std::chrono::duration<Rep,Period>const&timeout_duration);};}}recursive_timed_mutex provides an exclusive-ownership
recursive mutex. At most one fiber can own the lock on a given instance of
recursive_timed_mutex at any time. Multiple concurrent
calls to lock(),
try_lock(),
try_lock_for(),
try_lock_until()
and unlock()
shall be permitted. A fiber that already has exclusive ownership of a given
recursive_timed_mutex instance can call lock(),
try_lock(),
try_lock_for()
or try_lock_until()
to acquire an additional level of ownership of the mutex. unlock() must be called once for each level of ownership
acquired by a single fiber before ownership can be acquired by another fiber.
Member
function lock()
voidlock();Effects:
The current fiber blocks until ownership can be obtained.
Throws:
Nothing
Member
function try_lock()
booltry_lock()noexcept;Effects:
Attempt to obtain ownership for the current fiber without blocking.
Returns:true if ownership was
obtained for the current fiber, false
otherwise.
Throws:
Nothing.
Member
function unlock()
voidunlock();Effects:
Releases a lock on *this
by the current fiber.
Throws:lock_errorError Conditions:operation_not_permitted: if boost::this_fiber::get_id()
does not own the mutex.
Templated
member function try_lock_until()
template<typenameClock,typenameDuration>booltry_lock_until(std::chrono::time_point<Clock,Duration>const&timeout_time);Effects:
Attempt to obtain ownership for the current fiber. Blocks until ownership
can be obtained, or the specified time is reached. If the specified
time has already passed, behaves as recursive_timed_mutex::try_lock().
Returns:true if ownership was
obtained for the current fiber, false
otherwise.
Throws:
Timeout-related exceptions.
Templated
member function try_lock_for()
template<typenameRep,typenamePeriod>booltry_lock_for(std::chrono::duration<Rep,Period>const&timeout_duration);Effects:
Attempt to obtain ownership for the current fiber. Blocks until ownership
can be obtained, or the specified time is reached. If the specified
time has already passed, behaves as recursive_timed_mutex::try_lock().
Returns:true if ownership was
obtained for the current fiber, false
otherwise.
Throws:
Timeout-related exceptions.
Condition VariablesSynopsis
enumclasscv_status;{no_timeout,timeout};classcondition_variable;classcondition_variable_any;
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 calls
wait again to resume waiting.
In the simplest case, this condition is just a boolean variable:
boost::fibers::condition_variablecond;boost::fibers::mutexmtx;booldata_ready=false;voidprocess_data();voidwait_for_data_to_process(){{std::unique_lock<boost::fibers::mutex>lk(mtx);while(!data_ready){cond.wait(lk);}}// release lkprocess_data();}
Notice that the lk is passed
to condition_variable::wait(): wait() will atomically add the fiber to the set
of fibers waiting on the condition variable, and unlock the mutex.
When the fiber is awakened, the mutex
will be locked again before the call to wait() returns. This allows other fibers to acquire
the mutex in order to update
the shared data, and ensures that the data associated with the condition
is correctly synchronized.
wait_for_data_to_process() could equivalently be written:
voidwait_for_data_to_process(){{std::unique_lock<boost::fibers::mutex>lk(mtx);// make condition_variable::wait() perform the loopcond.wait(lk,[](){returndata_ready;});}// release lkprocess_data();}
In the meantime, another fiber sets data_ready
to true, and then calls either
condition_variable::notify_one() or condition_variable::notify_all() on
the condition_variablecond
to wake one waiting fiber or all the waiting fibers respectively.
voidretrieve_data();voidprepare_data();voidprepare_data_for_processing(){retrieve_data();prepare_data();{std::unique_lock<boost::fibers::mutex>lk(mtx);data_ready=true;}cond.notify_one();}
Note that the same mutex is locked before the shared data is updated,
but that the mutex does not
have to be locked across the call to condition_variable::notify_one().
Locking is important because the synchronization objects provided by Boost.Fiber can be used to synchronize fibers running
on different threads.
Boost.Fiber provides both condition_variable and
condition_variable_any. boost::fibers::condition_variable
can only wait on std::unique_lock<boost::fibers::mutex>
while boost::fibers::condition_variable_any can wait on user-defined
lock types.
No Spurious
Wakeups
Neither condition_variable nor condition_variable_any are
subject to spurious wakeup: condition_variable::wait() can
only wake up when condition_variable::notify_one() or
condition_variable::notify_all() is called. Even
so, it is prudent to use one of the wait(lock,predicate) overloads.
Consider a set of consumer fibers processing items from a std::queue.
The queue is continually populated by a set of producer fibers.
The consumer fibers might reasonably wait on a condition_variable
as long as the queue remains empty().
Because producer fibers might push()
items to the queue in bursts, they call condition_variable::notify_all() rather
than condition_variable::notify_one().
But a given consumer fiber might well wake up from condition_variable::wait() and
find the queue empty(),
because other consumer fibers might already have processed all pending items.
(See also spurious wakeup.)
Enumeration
cv_status
A timed wait operation might return because of timeout or not.
enumclasscv_status{no_timeout,timeout};no_timeoutEffects:
The condition variable was awakened with notify_one
or notify_all.
timeoutEffects:
The condition variable was awakened by timeout.
Class
condition_variable_any#include<boost/fiber/condition_variable.hpp>namespaceboost{namespacefibers{class condition_variable_any {public:
condition_variable_any();~condition_variable_any();
condition_variable_any( condition_variable_any const&)=delete;
condition_variable_any &operator=( condition_variable_any const&)=delete;voidnotify_one()noexcept;voidnotify_all()noexcept;
template< typename LockType >
void wait( LockType &);template< typename LockType, typename Pred>voidwait( LockType &,Pred);template< typename LockType, typename Clock,typenameDuration>cv_statuswait_until( LockType &,std::chrono::time_point<Clock,Duration>const&);template< typename LockType, typename Clock,typenameDuration,typenamePred>boolwait_until( LockType &,std::chrono::time_point<Clock,Duration>const&,Pred);template< typename LockType, typename Rep,typenamePeriod>cv_statuswait_for( LockType &,std::chrono::duration<Rep,Period>const&);template< typename LockType, typename Rep,typenamePeriod,typenamePred>boolwait_for( LockType &,std::chrono::duration<Rep,Period>const&,Pred);};}}Constructor
condition_variable_any()Effects:
Creates the object.
Throws:
Nothing.
Destructor
~condition_variable_any()Precondition:
All fibers waiting on *this have been notified by a call to
notify_one or notify_all (though the respective
calls to wait, wait_for or wait_until
need not have returned).
Effects:
Destroys the object.
Member
function notify_one()
voidnotify_one()noexcept;Effects:
If any fibers are currently blocked
waiting on *this
in a call to wait,
wait_for or wait_until, unblocks one of those
fibers.
Throws:
Nothing.
Note:
It is arbitrary which waiting fiber is resumed.
Member
function notify_all()
voidnotify_all()noexcept;Effects:
If any fibers are currently blocked
waiting on *this
in a call to wait,
wait_for or wait_until, unblocks all of those
fibers.
Throws:
Nothing.
Note:
This is why a waiting fiber must also check for
the desired program state using a mechanism external to the condition_variable_any,
and retry the wait until that state is reached. A fiber waiting on
a condition_variable_any might well wake up a number of times before
the desired state is reached.
Templated
member function wait()
template< typename LockType >
void wait( LockType &lk);template< typename LockType, typename Pred>voidwait( LockType &lk,Predpred);Precondition:lk is locked by the
current fiber, and either no other fiber is currently waiting on *this,
or the execution of the mutex()
member function on the lk
objects supplied in the calls to wait
in all the fibers currently waiting on *this would return the same value as
lk->mutex()
for this call to wait.
Effects:
Atomically call lk.unlock() and blocks the current fiber. The
fiber will unblock when notified by a call to this->notify_one() or this->notify_all(). When the fiber is unblocked (for
whatever reason), the lock is reacquired by invoking lk.lock()
before the call to wait
returns. The lock is also reacquired by invoking lk.lock() if the function exits with an exception.
The member function accepting pred
is shorthand for:
while(!pred()){wait(lk);}Postcondition:lk is locked by the
current fiber.
Throws:fiber_error if an error
occurs.
Note:
The Precondition is a bit dense. It merely states that all the fibers
concurrently calling wait
on *this
must wait on lk objects
governing the samemutex. Three distinct
objects are involved in any condition_variable_any::wait() call: the
condition_variable_any itself, the mutex
coordinating access between fibers and a local lock object (e.g. std::unique_lock). In general,
you can partition the lifespan of a given condition_variable_any instance
into periods with one or more fibers waiting on it, separated by periods
when no fibers are waiting on it. When more than one fiber is waiting
on that condition_variable_any, all must pass lock objects referencing
the samemutex
instance.
Templated
member function wait_until()
template< typename LockType, typename Clock,typenameDuration>cv_statuswait_until( LockType &lk,std::chrono::time_point<Clock,Duration>const&abs_time);template< typename LockType, typename Clock,typenameDuration,typenamePred>boolwait_until( LockType &lk,std::chrono::time_point<Clock,Duration>const&abs_time,Predpred);Precondition:lk is locked by the
current fiber, and either no other fiber is currently waiting on *this,
or the execution of the mutex() member function on the lk objects supplied in the calls
to wait, wait_for or wait_until
in all the fibers currently waiting on *this would return the same value as
lk.mutex()
for this call to wait_until.
Effects:
Atomically call lk.unlock() and blocks the current fiber. The
fiber will unblock when notified by a call to this->notify_one() or this->notify_all(), when the system time would be equal
to or later than the specified abs_time.
When the fiber is unblocked (for whatever reason), the lock is reacquired
by invoking lk.lock()
before the call to wait_until
returns. The lock is also reacquired by invoking lk.lock() if the function exits with an exception.
The member function accepting pred
is shorthand for:
while(!pred()){if(cv_status::timeout==wait_until(lk,abs_time))returnpred();}returntrue;
That is, even if wait_until() times out, it can still return true if pred() returns true
at that time.
Postcondition:lk is locked by the
current fiber.
Throws:fiber_error if an error
occurs or timeout-related exceptions.
Returns:
The overload without pred
returns cv_status::no_timeout if awakened by notify_one()
or notify_all(),
or cv_status::timeout if awakened because the system
time is past abs_time.
Returns:
The overload accepting pred
returns false if the call
is returning because the time specified by abs_time
was reached and the predicate returns false,
true otherwise.
Note:
See Note for condition_variable_any::wait().
Templated
member function wait_for()
template< typename LockType, typename Rep,typenamePeriod>cv_statuswait_for( LockType &lk,std::chrono::duration<Rep,Period>const&rel_time);template< typename LockType, typename Rep,typenamePeriod,typenamePred>boolwait_for( LockType &lk,std::chrono::duration<Rep,Period>const&rel_time,Predpred);Precondition:lk is locked by the
current fiber, and either no other fiber is currently waiting on *this,
or the execution of the mutex() member function on the lk objects supplied in the calls
to wait, wait_for or wait_until
in all the fibers currently waiting on *this would return the same value as
lk.mutex()
for this call to wait_for.
Effects:
Atomically call lk.unlock() and blocks the current fiber. The
fiber will unblock when notified by a call to this->notify_one() or this->notify_all(), when a time interval equal to or
greater than the specified rel_time
has elapsed. When the fiber is unblocked (for whatever reason), the
lock is reacquired by invoking lk.lock() before the call to wait returns. The lock is also reacquired
by invoking lk.lock()
if the function exits with an exception. The wait_for() member function accepting pred is shorthand for:
while(!pred()){if(cv_status::timeout==wait_for(lk,rel_time)){returnpred();}}returntrue;
(except of course that rel_time
is adjusted for each iteration). The point is that, even if wait_for()
times out, it can still return true
if pred()
returns true at that time.
Postcondition:lk is locked by the
current fiber.
Throws:fiber_error if an error
occurs or timeout-related exceptions.
Returns:
The overload without pred
returns cv_status::no_timeout if awakened by notify_one()
or notify_all(),
or cv_status::timeout if awakened because at least
rel_time has elapsed.
Returns:
The overload accepting pred
returns false if the call
is returning because at least rel_time
has elapsed and the predicate returns false,
true otherwise.
Note:
See Note for condition_variable_any::wait().
Class
condition_variable#include<boost/fiber/condition_variable.hpp>namespaceboost{namespacefibers{class condition_variable {public:
condition_variable();~condition_variable();
condition_variable( condition_variable const&)=delete;
condition_variable &operator=( condition_variable const&)=delete;voidnotify_one()noexcept;voidnotify_all()noexcept;
void wait( std::unique_lock< mutex > &);template< typename Pred>voidwait( std::unique_lock< mutex > &,Pred);template< typename Clock,typenameDuration>cv_statuswait_until( std::unique_lock< mutex > &,std::chrono::time_point<Clock,Duration>const&);template< typename Clock,typenameDuration,typenamePred>boolwait_until( std::unique_lock< mutex > &,std::chrono::time_point<Clock,Duration>const&,Pred);template< typename Rep,typenamePeriod>cv_statuswait_for( std::unique_lock< mutex > &,std::chrono::duration<Rep,Period>const&);template< typename Rep,typenamePeriod,typenamePred>boolwait_for( std::unique_lock< mutex > &,std::chrono::duration<Rep,Period>const&,Pred);};}}Constructor
condition_variable()Effects:
Creates the object.
Throws:
Nothing.
Destructor
~condition_variable()Precondition:
All fibers waiting on *this have been notified by a call to
notify_one or notify_all (though the respective
calls to wait, wait_for or wait_until
need not have returned).
Effects:
Destroys the object.
Member
function notify_one()
voidnotify_one()noexcept;Effects:
If any fibers are currently blocked
waiting on *this
in a call to wait,
wait_for or wait_until, unblocks one of those
fibers.
Throws:
Nothing.
Note:
It is arbitrary which waiting fiber is resumed.
Member
function notify_all()
voidnotify_all()noexcept;Effects:
If any fibers are currently blocked
waiting on *this
in a call to wait,
wait_for or wait_until, unblocks all of those
fibers.
Throws:
Nothing.
Note:
This is why a waiting fiber must also check for
the desired program state using a mechanism external to the condition_variable,
and retry the wait until that state is reached. A fiber waiting on
a condition_variable might well wake up a number of times before the
desired state is reached.
Templated
member function wait()
void wait( std::unique_lock< mutex > &lk);template< typename Pred>voidwait( std::unique_lock< mutex > &lk,Predpred);Precondition:lk is locked by the
current fiber, and either no other fiber is currently waiting on *this,
or the execution of the mutex()
member function on the lk
objects supplied in the calls to wait
in all the fibers currently waiting on *this would return the same value as
lk->mutex()
for this call to wait.
Effects:
Atomically call lk.unlock() and blocks the current fiber. The
fiber will unblock when notified by a call to this->notify_one() or this->notify_all(). When the fiber is unblocked (for
whatever reason), the lock is reacquired by invoking lk.lock()
before the call to wait
returns. The lock is also reacquired by invoking lk.lock() if the function exits with an exception.
The member function accepting pred
is shorthand for:
while(!pred()){wait(lk);}Postcondition:lk is locked by the
current fiber.
Throws:fiber_error if an error
occurs.
Note:
The Precondition is a bit dense. It merely states that all the fibers
concurrently calling wait
on *this
must wait on lk objects
governing the samemutex. Three distinct
objects are involved in any condition_variable::wait() call: the condition_variable itself,
the mutex coordinating
access between fibers and a local lock object (e.g. std::unique_lock). In general,
you can partition the lifespan of a given condition_variable instance
into periods with one or more fibers waiting on it, separated by periods
when no fibers are waiting on it. When more than one fiber is waiting
on that condition_variable, all must pass lock objects referencing
the samemutex
instance.
Templated
member function wait_until()
template< typename Clock,typenameDuration>cv_statuswait_until( std::unique_lock< mutex > &lk,std::chrono::time_point<Clock,Duration>const&abs_time);template< typename Clock,typenameDuration,typenamePred>boolwait_until( std::unique_lock< mutex > &lk,std::chrono::time_point<Clock,Duration>const&abs_time,Predpred);Precondition:lk is locked by the
current fiber, and either no other fiber is currently waiting on *this,
or the execution of the mutex() member function on the lk objects supplied in the calls
to wait, wait_for or wait_until
in all the fibers currently waiting on *this would return the same value as
lk.mutex()
for this call to wait_until.
Effects:
Atomically call lk.unlock() and blocks the current fiber. The
fiber will unblock when notified by a call to this->notify_one() or this->notify_all(), when the system time would be equal
to or later than the specified abs_time.
When the fiber is unblocked (for whatever reason), the lock is reacquired
by invoking lk.lock()
before the call to wait_until
returns. The lock is also reacquired by invoking lk.lock() if the function exits with an exception.
The member function accepting pred
is shorthand for:
while(!pred()){if(cv_status::timeout==wait_until(lk,abs_time))returnpred();}returntrue;
That is, even if wait_until() times out, it can still return true if pred() returns true
at that time.
Postcondition:lk is locked by the
current fiber.
Throws:fiber_error if an error
occurs or timeout-related exceptions.
Returns:
The overload without pred
returns cv_status::no_timeout if awakened by notify_one()
or notify_all(),
or cv_status::timeout if awakened because the system
time is past abs_time.
Returns:
The overload accepting pred
returns false if the call
is returning because the time specified by abs_time
was reached and the predicate returns false,
true otherwise.
Note:
See Note for condition_variable::wait().
Templated
member function wait_for()
template< typename Rep,typenamePeriod>cv_statuswait_for( std::unique_lock< mutex > &lk,std::chrono::duration<Rep,Period>const&rel_time);template< typename Rep,typenamePeriod,typenamePred>boolwait_for( std::unique_lock< mutex > &lk,std::chrono::duration<Rep,Period>const&rel_time,Predpred);Precondition:lk is locked by the
current fiber, and either no other fiber is currently waiting on *this,
or the execution of the mutex() member function on the lk objects supplied in the calls
to wait, wait_for or wait_until
in all the fibers currently waiting on *this would return the same value as
lk.mutex()
for this call to wait_for.
Effects:
Atomically call lk.unlock() and blocks the current fiber. The
fiber will unblock when notified by a call to this->notify_one() or this->notify_all(), when a time interval equal to or
greater than the specified rel_time
has elapsed. When the fiber is unblocked (for whatever reason), the
lock is reacquired by invoking lk.lock() before the call to wait returns. The lock is also reacquired
by invoking lk.lock()
if the function exits with an exception. The wait_for() member function accepting pred is shorthand for:
while(!pred()){if(cv_status::timeout==wait_for(lk,rel_time)){returnpred();}}returntrue;
(except of course that rel_time
is adjusted for each iteration). The point is that, even if wait_for()
times out, it can still return true
if pred()
returns true at that time.
Postcondition:lk is locked by the
current fiber.
Throws:fiber_error if an error
occurs or timeout-related exceptions.
Returns:
The overload without pred
returns cv_status::no_timeout if awakened by notify_one()
or notify_all(),
or cv_status::timeout if awakened because at least
rel_time has elapsed.
Returns:
The overload accepting pred
returns false if the call
is returning because at least rel_time
has elapsed and the predicate returns false,
true otherwise.
Note:
See Note for condition_variable::wait().
Barriers
A barrier is a concept also known as a rendezvous, it
is a synchronization point between multiple contexts of execution (fibers).
The barrier is configured for a particular number of fibers (n), and as fibers reach the barrier they
must wait until all n fibers
have arrived. Once the n-th
fiber has reached the barrier, all the waiting fibers can proceed, and the
barrier is reset.
The fact that the barrier automatically resets is significant. Consider a
case in which you launch some number of fibers and want to wait only until
the first of them has completed. You might be tempted to use a barrier(2) as the synchronization
mechanism, making each new fiber call its barrier::wait() method,
then calling wait()
in the launching fiber to wait until the first other fiber completes.
That will in fact unblock the launching fiber. The unfortunate part is that
it will continue blocking the remaining fibers.
Consider the following scenario:
Fiber main launches fibers A, B, C and D, then calls
barrier::wait().
Fiber C finishes first and likewise calls barrier::wait().
Fiber main is unblocked, as desired.
Fiber B calls barrier::wait(). Fiber B is blocked!
Fiber A calls barrier::wait(). Fibers A and B are unblocked.
Fiber D calls barrier::wait(). Fiber D is blocked indefinitely.
(See also when_any, simple completion.)
It is unwise to tie the lifespan of a barrier to any one of its participating
fibers. Although conceptually all waiting fibers awaken simultaneously,
because of the nature of fibers, in practice they will awaken one by one
in indeterminate order.
The current implementation wakes fibers in FIFO order: the first to call
wait()
wakes first, and so forth. But it is perilous to rely on the order in
which the various fibers will reach the wait() call.
The rest of the waiting fibers will still be blocked in wait(),
which must, before returning, access data members in the barrier object.
Class barrier#include<boost/fiber/barrier.hpp>namespaceboost{namespacefibers{classbarrier{public:explicitbarrier(std::size_t);barrier(barrierconst&)=delete;barrier&operator=(barrierconst&)=delete;boolwait();};}}
Instances of barrier are not copyable or movable.
Constructor
explicitbarrier(std::size_tinitial);Effects:
Construct a barrier for initial
fibers.
Throws:fiber_errorError Conditions:invalid_argument: if initial is zero.
Member function wait()
boolwait();Effects:
Block until initial
fibers have called wait
on *this.
When the initial-th
fiber calls wait, all
waiting fibers are unblocked, and the barrier is reset.
Returns:true for exactly one fiber
from each batch of waiting fibers, false
otherwise.
Throws:fiber_errorChannels
A channel is a model to communicate and synchronize ThreadsofExecution
The smallest ordered sequence of instructions that can be managed independently
by a scheduler is called a ThreadofExecution.
via message passing.
Enumeration
channel_op_status
channel operations return the state of the channel.
enumclasschannel_op_status{success,empty,full,closed,timeout};successEffects:
Operation was successful.
emptyEffects:
channel is empty, operation failed.
fullEffects:
channel is full, operation failed.
closedEffects:
channel is closed, operation failed.
timeoutEffects:
The operation did not become ready before specified timeout elapsed.
Buffered
ChannelBoost.Fiber provides a bounded, buffered
channel (MPMC queue) suitable to synchonize fibers (running on same or
different threads) via asynchronouss message passing.
typedefboost::fibers::buffered_channel<int>channel_t;voidsend(channel_t&chan){for(inti=0;i<5;++i){chan.push(i);}chan.close();}voidrecv(channel_t&chan){inti;while(boost::fibers::channel_op_status::success==chan.pop(i)){std::cout<<"received "<<i<<std::endl;}}channel_tchan{2};boost::fibers::fiberf1(std::bind(send,std::ref(chan)));boost::fibers::fiberf2(std::bind(recv,std::ref(chan)));f1.join();f2.join();
Class buffered_channel
supports range-for syntax:
typedefboost::fibers::buffered_channel<int>channel_t;voidfoo(channel_t&chan){chan.push(1);chan.push(1);chan.push(2);chan.push(3);chan.push(5);chan.push(8);chan.push(12);chan.close();}voidbar(channel_t&chan){for(unsignedintvalue:chan){std::cout<<value<<" ";}std::cout<<std::endl;}
Template
buffered_channel<>#include<boost/fiber/buffered_channel.hpp>namespaceboost{namespacefibers{template<typenameT>classbuffered_channel{public:typedefTvalue_type;classiterator;explicitbuffered_channel(std::size_tcapacity);buffered_channel(buffered_channelconst&other)=delete;buffered_channel&operator=(buffered_channelconst&other)=delete;voidclose()noexcept;channel_op_statuspush(value_typeconst&va);channel_op_statuspush(value_type&&va);template<typenameRep,typenamePeriod>channel_op_statuspush_wait_for(value_typeconst&va,std::chrono::duration<Rep,Period>const&timeout_duration);channel_op_statuspush_wait_for(value_type&&va,std::chrono::duration<Rep,Period>const&timeout_duration);template<typenameClock,typenameDuration>channel_op_statuspush_wait_until(value_typeconst&va,std::chrono::time_point<Clock,Duration>const&timeout_time);template<typenameClock,typenameDuration>channel_op_statuspush_wait_until(value_type&&va,std::chrono::time_point<Clock,Duration>const&timeout_time);channel_op_statustry_push(value_typeconst&va);channel_op_statustry_push(value_type&&va);channel_op_statuspop(value_type&va);value_typevalue_pop();template<typenameRep,typenamePeriod>channel_op_statuspop_wait_for(value_type&va,std::chrono::duration<Rep,Period>const&timeout_duration);template<typenameClock,typenameDuration>channel_op_statuspop_wait_until(value_type&va,std::chrono::time_point<Clock,Duration>const&timeout_time);channel_op_statustry_pop(value_type&va);};template<typenameT>buffered_channel<T>::iteratorbegin(buffered_channel<T>&chan);template<typenameT>buffered_channel<T>::iteratorend(buffered_channel<T>&chan);}}Constructor
explicitbuffered_channel(std::size_tcapacity);Preconditions:2<=capacity&&0==(capacity&(capacity-1))Effects:
The constructor constructs an object of class buffered_channel
with an internal buffer of size capacity.
Throws:fiber_errorError Conditions:invalid_argument: if 0==capacity||0!=(capacity&(capacity-1)).
Notes:
A push(),
push_wait_for()
or push_wait_until() will not block until the number
of values in the channel becomes equal to capacity.
The channel can hold only capacity-1
elements, otherwise it is considered to be full.
Member
function close()
voidclose()noexcept;Effects:
Deactivates the channel. No values can be put after calling this->close().
Fibers blocked in this->pop(), this->pop_wait_for() or this->pop_wait_until() will return closed.
Fibers blocked in this->value_pop() will receive an exception.
Throws:
Nothing.
Note:close()
is like closing a pipe. It informs waiting consumers that no more
values will arrive.
Member
function push()
channel_op_statuspush(value_typeconst&va);channel_op_statuspush(value_type&&va);Effects:
If channel is closed, returns closed.
Otherwise enqueues the value in the channel, wakes up a fiber blocked
on this->pop(),
this->value_pop(),
this->pop_wait_for()
or this->pop_wait_until()
and returns success.
If the channel is full, the fiber is blocked.
Throws:
Exceptions thrown by copy- or move-operations.
Member
function try_push()
channel_op_statustry_push(value_typeconst&va);channel_op_statustry_push(value_type&&va);Effects:
If channel is closed, returns closed.
Otherwise enqueues the value in the channel, wakes up a fiber blocked
on this->pop(),
this->value_pop(),
this->pop_wait_for()
or this->pop_wait_until()
and returns success.
If the channel is full, it doesn't block and returns full.
Throws:
Exceptions thrown by copy- or move-operations.
Member
function pop()
channel_op_statuspop(value_type&va);Effects:
Dequeues a value from the channel. If the channel is empty, the fiber
gets suspended until at least one new item is push()ed (return value success
and va contains dequeued
value) or the channel gets close()d (return value closed).
Throws:
Exceptions thrown by copy- or move-operations.
Member
function value_pop()
value_typevalue_pop();Effects:
Dequeues a value from the channel. If the channel is empty, the fiber
gets suspended until at least one new item is push()ed or the channel gets close()d
(which throws an exception).
Throws:fiber_error if *this
is closed or by copy- or move-operations.
Error conditions:std::errc::operation_not_permitted
Member
function try_pop()
channel_op_statustry_pop(value_type&va);Effects:
If channel is empty, returns empty.
If channel is closed, returns closed.
Otherwise it returns success
and va contains the
dequeued value.
Throws:
Exceptions thrown by copy- or move-operations.
Member
function pop_wait_for()
template<typenameRep,typenamePeriod>channel_op_statuspop_wait_for(value_type&va,std::chrono::duration<Rep,Period>const&timeout_duration)Effects:
Accepts std::chrono::duration and internally computes
a timeout time as (system time + timeout_duration).
If channel is not empty, immediately dequeues a value from the channel.
Otherwise the fiber gets suspended until at least one new item is
push()ed
(return value success
and va contains dequeued
value), or the channel gets close()d (return value closed),
or the system time reaches the computed timeout time (return value
timeout).
Throws:
timeout-related exceptions or by copy- or move-operations.
Member
function pop_wait_until()
template<typenameClock,typenameDuration>channel_op_statuspop_wait_until(value_type&va,std::chrono::time_point<Clock,Duration>const&timeout_time)Effects:
Accepts a std::chrono::time_point<Clock,Duration>.
If channel is not empty, immediately dequeues a value from the channel.
Otherwise the fiber gets suspended until at least one new item is
push()ed
(return value success
and va contains dequeued
value), or the channel gets close()d (return value closed),
or the system time reaches the passed time_point
(return value timeout).
Throws:
timeout-related exceptions or by copy- or move-operations.
Non-member
function begin(buffered_channel<T>&)template<typenameT>buffered_channel<T>::iteratorbegin(buffered_channel<T>&);Returns:
Returns a range-iterator (input-iterator).
Non-member
function end(buffered_channel<T>&)template<typenameT>buffered_channel<R>::iteratorend(buffered_channel<T>&);Returns:
Returns an end range-iterator (input-iterator).
Unbuffered
ChannelBoost.Fiber provides template unbuffered_channel suitable to synchonize
fibers (running on same or different threads) via synchronous message passing.
A fiber waiting to consume an value will block until the value is produced.
If a fiber attempts to send a value through an unbuffered channel and no
fiber is waiting to receive the value, the channel will block the sending
fiber.
The unbuffered channel acts as an rendezvouspoint.
typedefboost::fibers::unbuffered_channel<int>channel_t;voidsend(channel_t&chan){for(inti=0;i<5;++i){chan.push(i);}chan.close();}voidrecv(channel_t&chan){inti;while(boost::fibers::channel_op_status::success==chan.pop(i)){std::cout<<"received "<<i<<std::endl;}}channel_tchan{1};boost::fibers::fiberf1(std::bind(send,std::ref(chan)));boost::fibers::fiberf2(std::bind(recv,std::ref(chan)));f1.join();f2.join();
Range-for syntax is supported:
typedefboost::fibers::unbuffered_channel<int>channel_t;voidfoo(channel_t&chan){chan.push(1);chan.push(1);chan.push(2);chan.push(3);chan.push(5);chan.push(8);chan.push(12);chan.close();}voidbar(channel_t&chan){for(unsignedintvalue:chan){std::cout<<value<<" ";}std::cout<<std::endl;}
Template
unbuffered_channel<>#include<boost/fiber/unbuffered_channel.hpp>namespaceboost{namespacefibers{template<typenameT>classunbuffered_channel{public:typedefTvalue_type;classiterator;unbuffered_channel();unbuffered_channel(unbuffered_channelconst&other)=delete;unbuffered_channel&operator=(unbuffered_channelconst&other)=delete;voidclose()noexcept;channel_op_statuspush(value_typeconst&va);channel_op_statuspush(value_type&&va);template<typenameRep,typenamePeriod>channel_op_statuspush_wait_for(value_typeconst&va,std::chrono::duration<Rep,Period>const&timeout_duration);channel_op_statuspush_wait_for(value_type&&va,std::chrono::duration<Rep,Period>const&timeout_duration);template<typenameClock,typenameDuration>channel_op_statuspush_wait_until(value_typeconst&va,std::chrono::time_point<Clock,Duration>const&timeout_time);template<typenameClock,typenameDuration>channel_op_statuspush_wait_until(value_type&&va,std::chrono::time_point<Clock,Duration>const&timeout_time);channel_op_statuspop(value_type&va);value_typevalue_pop();template<typenameRep,typenamePeriod>channel_op_statuspop_wait_for(value_type&va,std::chrono::duration<Rep,Period>const&timeout_duration);template<typenameClock,typenameDuration>channel_op_statuspop_wait_until(value_type&va,std::chrono::time_point<Clock,Duration>const&timeout_time);};template<typenameT>unbuffered_channel<T>::iteratorbegin(unbuffered_channel<T>&chan);template<typenameT>unbuffered_channel<T>::iteratorend(unbuffered_channel<T>&chan);}}Constructor
unbuffered_channel();Effects:
The constructor constructs an object of class unbuffered_channel.
Member
function close()
voidclose()noexcept;Effects:
Deactivates the channel. No values can be put after calling this->close().
Fibers blocked in this->pop(), this->pop_wait_for() or this->pop_wait_until() will return closed.
Fibers blocked in this->value_pop() will receive an exception.
Throws:
Nothing.
Note:close()
is like closing a pipe. It informs waiting consumers that no more
values will arrive.
Member
function push()
channel_op_statuspush(value_typeconst&va);channel_op_statuspush(value_type&&va);Effects:
If channel is closed, returns closed.
Otherwise enqueues the value in the channel, wakes up a fiber blocked
on this->pop(),
this->value_pop(),
this->pop_wait_for()
or this->pop_wait_until()
and returns success.
Throws:
Exceptions thrown by copy- or move-operations.
Member
function pop()
channel_op_statuspop(value_type&va);Effects:
Dequeues a value from the channel. If the channel is empty, the fiber
gets suspended until at least one new item is push()ed (return value success
and va contains dequeued
value) or the channel gets close()d (return value closed).
Throws:
Exceptions thrown by copy- or move-operations.
Member
function value_pop()
value_typevalue_pop();Effects:
Dequeues a value from the channel. If the channel is empty, the fiber
gets suspended until at least one new item is push()ed or the channel gets close()d
(which throws an exception).
Throws:fiber_error if *this
is closed or by copy- or move-operations.
Error conditions:std::errc::operation_not_permitted
Member
function pop_wait_for()
template<typenameRep,typenamePeriod>channel_op_statuspop_wait_for(value_type&va,std::chrono::duration<Rep,Period>const&timeout_duration)Effects:
Accepts std::chrono::duration and internally computes
a timeout time as (system time + timeout_duration).
If channel is not empty, immediately dequeues a value from the channel.
Otherwise the fiber gets suspended until at least one new item is
push()ed
(return value success
and va contains dequeued
value), or the channel gets close()d (return value closed),
or the system time reaches the computed timeout time (return value
timeout).
Throws:
timeout-related exceptions or by copy- or move-operations.
Member
function pop_wait_until()
template<typenameClock,typenameDuration>channel_op_statuspop_wait_until(value_type&va,std::chrono::time_point<Clock,Duration>const&timeout_time)Effects:
Accepts a std::chrono::time_point<Clock,Duration>.
If channel is not empty, immediately dequeues a value from the channel.
Otherwise the fiber gets suspended until at least one new item is
push()ed
(return value success
and va contains dequeued
value), or the channel gets close()d (return value closed),
or the system time reaches the passed time_point
(return value timeout).
Throws:
timeout-related exceptions or by copy- or move-operations.
Non-member
function begin(unbuffered_channel<T>&)template<typenameT>unbuffered_channel<T>::iteratorbegin(unbuffered_channel<T>&);Returns:
Returns a range-iterator (input-iterator).
Non-member
function end(unbuffered_channel<T>&)template<typenameT>unbuffered_channel<R>::iteratorend(unbuffered_channel<T>&);Returns:
Returns an end range-iterator (input-iterator).
FuturesOverview
The futures library provides a means of handling asynchronous future values,
whether those values are generated by another fiber, or on a single fiber
in response to external stimuli, or on-demand.
This is done through the provision of four class templates: future<> and
shared_future<> which are used to retrieve the asynchronous
results, and promise<> and packaged_task<> which
are used to generate the asynchronous results.
An instance of future<> holds the one and only reference
to a result. Ownership can be transferred between instances using the move
constructor or move-assignment operator, but at most one instance holds a
reference to a given asynchronous result. When the result is ready, it is
returned from future::get() by rvalue-reference to allow the result
to be moved or copied as appropriate for the type.
On the other hand, many instances of shared_future<> may
reference the same result. Instances can be freely copied and assigned, and
shared_future::get()
returns a const
reference so that multiple calls to shared_future::get()
are
safe. You can move an instance of future<> into an instance
of shared_future<>, thus transferring ownership
of the associated asynchronous result, but not vice-versa.
fibers::async() is a simple way of running asynchronous tasks.
A call to async()
spawns a fiber and returns a future<> that will deliver
the result of the fiber function.
Creating
asynchronous values
You can set the value in a future with either a promise<> or
a packaged_task<>. A packaged_task<> is
a callable object with void
return that wraps a function or callable object returning the specified type.
When the packaged_task<> is invoked, it invokes the
contained function in turn, and populates a future with the contained function's
return value. This is an answer to the perennial question: How do
I return a value from a fiber? Package the function you wish to run
as a packaged_task<> and pass the packaged task to
the fiber constructor. The future retrieved from the packaged task can then
be used to obtain the return value. If the function throws an exception,
that is stored in the future in place of the return value.
intcalculate_the_answer_to_life_the_universe_and_everything(){return42;}boost::fibers::packaged_task<int()>pt(calculate_the_answer_to_life_the_universe_and_everything);boost::fibers::future<int>fi=pt.get_future();boost::fibers::fiber(std::move(pt)).detach();// launch task on a fiberfi.wait();// wait for it to finishassert(fi.is_ready());assert(fi.has_value());assert(!fi.has_exception());assert(fi.get()==42);
A promise<> is a bit more low level: it just provides explicit
functions to store a value or an exception in the associated future. A promise
can therefore be used where the value might come from more than one possible
source.
boost::fibers::promise<int>pi;boost::fibers::future<int>fi;fi=pi.get_future();pi.set_value(42);assert(fi.is_ready());assert(fi.has_value());assert(!fi.has_exception());assert(fi.get()==42);Future
A future provides a mechanism to access the result of an asynchronous operation.
shared
state
Behind a promise<> and its future<> lies
an unspecified object called their shared state. The
shared state is what will actually hold the async result (or the exception).
The shared state is instantiated along with the promise<>.
Aside from its originating promise<>, a future<> holds
a unique reference to a particular shared state. However, multiple shared_future<> instances
can reference the same underlying shared state.
As packaged_task<> and fibers::async() are
implemented using promise<>, discussions of shared state
apply to them as well.
Enumeration
future_status
Timed wait-operations (future::wait_for() and future::wait_until())
return the state of the future.
enumclassfuture_status{ready,timeout,deferred// not supported yet};readyEffects:
The shared state is ready.
timeoutEffects:
The shared state did not become
ready before timeout has passed.
Deferred futures are not supported.
Template future<>
A future<> contains a shared
state which is not shared with any other future.
#include<boost/fiber/future/future.hpp>namespaceboost{namespacefibers{template<typenameR>classfuture{public:future()noexcept;future(futureconst&other)=delete;future&operator=(futureconst&other)=delete;future(future&&other)noexcept;future&operator=(future&&other)noexcept;~future();boolvalid()constnoexcept;shared_future<R>share();Rget();// member only of generic future templateR&get();// member only of future< R & > template specializationvoidget();// member only of future< void > template specializationstd::exception_ptrget_exception_ptr();voidwait()const;template<classRep,classPeriod>future_statuswait_for(std::chrono::duration<Rep,Period>const&timeout_duration)const;template<typenameClock,typenameDuration>future_statuswait_until(std::chrono::time_point<Clock,Duration>const&timeout_time)const;};}}Default
constructor
future()noexcept;Effects:
Creates a future with no shared state.
After construction false==valid().
Throws:
Nothing.
Move constructor
future(future&&other)noexcept;Effects:
Constructs a future with the shared
state of other. After construction false==other.valid().
Throws:
Nothing.
Destructor
~future();Effects:
Destroys the future; ownership is abandoned.
Note:~future() does not block the calling fiber.
Consider a sequence such as:
instantiate promise<>
obtain its future<>
via promise::get_future()
launch fiber, capturing promise<>
destroy future<>
call promise::set_value()
The final set_value()
call succeeds, but the value is silently discarded: no additional future<>
can be obtained from that promise<>.
Member
function operator=()
future&operator=(future&&other)noexcept;Effects:
Moves the shared state of other
to this. After the assignment,
false==other.valid().
Throws:
Nothing.
Member function valid()
boolvalid()constnoexcept;Effects:
Returns true if future
contains a shared state.
Throws:
Nothing.
Member function share()
shared_future<R>share();Effects:
Move the state to a shared_future<>.
Returns:
a shared_future<> containing the shared
state formerly belonging to *this.
Postcondition:false==valid()Throws:future_error with
error condition future_errc::no_state.
Member function get()
Rget();// member only of generic future templateR&get();// member only of future< R & > template specializationvoidget();// member only of future< void > template specializationPrecondition:true==valid()Returns:
Waits until promise::set_value() or promise::set_exception() is
called. If promise::set_value() is called, returns
the value. If promise::set_exception() is called,
throws the indicated exception.
Postcondition:false==valid()Throws:future_error with
error condition future_errc::no_state,
future_errc::broken_promise. Any exception passed
to promise::set_exception().
Member
function get_exception_ptr()
std::exception_ptrget_exception_ptr();Precondition:true==valid()Returns:
Waits until promise::set_value() or promise::set_exception() is
called. If set_value() is called, returns a default-constructed
std::exception_ptr. If set_exception()
is called, returns the passed std::exception_ptr.
Throws:future_error with
error condition future_errc::no_state.
Note:get_exception_ptr() does not invalidate
the future. After calling get_exception_ptr(), you may still call future::get().
Member function wait()
voidwait();Effects:
Waits until promise::set_value() or promise::set_exception() is
called.
Throws:future_error with
error condition future_errc::no_state.
Templated member
function wait_for()
template<classRep,classPeriod>future_statuswait_for(std::chrono::duration<Rep,Period>const&timeout_duration)const;Effects:
Waits until promise::set_value() or promise::set_exception() is
called, or timeout_duration
has passed.
Result:
A future_status is
returned indicating the reason for returning.
Throws:future_error with
error condition future_errc::no_state
or timeout-related exceptions.
Templated
member function wait_until()
template<typenameClock,typenameDuration>future_statuswait_until(std::chrono::time_point<Clock,Duration>const&timeout_time)const;Effects:
Waits until promise::set_value() or promise::set_exception() is
called, or timeout_time
has passed.
Result:
A future_status is
returned indicating the reason for returning.
Throws:future_error with
error condition future_errc::no_state
or timeout-related exceptions.
Template
shared_future<>
A shared_future<> contains a shared
state which might be shared with other shared_future<> instances.
#include<boost/fiber/future/future.hpp>namespaceboost{namespacefibers{template<typenameR>classshared_future{public:shared_future()noexcept;~shared_future();shared_future(shared_futureconst&other);shared_future(future<R>&&other)noexcept;shared_future(shared_future&&other)noexcept;shared_future&operator=(shared_future&&other)noexcept;shared_future&operator=(future<R>&&other)noexcept;shared_future&operator=(shared_futureconst&other)noexcept;boolvalid()constnoexcept;Rconst&get();// member only of generic shared_future templateR&get();// member only of shared_future< R & > template specializationvoidget();// member only of shared_future< void > template specializationstd::exception_ptrget_exception_ptr();voidwait()const;template<classRep,classPeriod>future_statuswait_for(std::chrono::duration<Rep,Period>const&timeout_duration)const;template<typenameClock,typenameDuration>future_statuswait_until(std::chrono::time_point<Clock,Duration>const&timeout_time)const;};}}Default
constructor
shared_future();Effects:
Creates a shared_future with no shared
state. After construction false==valid().
Throws:
Nothing.
Move constructor
shared_future(future<R>&&other)noexcept;shared_future(shared_future&&other)noexcept;Effects:
Constructs a shared_future with the shared
state of other. After construction false==other.valid().
Throws:
Nothing.
Copy constructor
shared_future(shared_futureconst&other)noexcept;Effects:
Constructs a shared_future with the shared
state of other. After construction other.valid() is unchanged.
Throws:
Nothing.
Destructor
~shared_future();Effects:
Destroys the shared_future; ownership is abandoned if not shared.
Note:~shared_future() does not block the calling fiber.
Member
function operator=()
shared_future&operator=(future<R>&&other)noexcept;shared_future&operator=(shared_future&&other)noexcept;shared_future&operator=(shared_futureconst&other)noexcept;Effects:
Moves or copies the shared state
of other to this. After
the assignment, the state of other.valid() depends on which overload was invoked:
unchanged for the overload accepting shared_futureconst&,
otherwise false.
Throws:
Nothing.
Member
function valid()
boolvalid()constnoexcept;Effects:
Returns true if shared_future
contains a shared state.
Throws:
Nothing.
Member function
get()
Rconst&get();// member only of generic shared_future templateR&get();// member only of shared_future< R & > template specializationvoidget();// member only of shared_future< void > template specializationPrecondition:true==valid()Returns:
Waits until promise::set_value() or promise::set_exception() is
called. If promise::set_value() is called, returns
the value. If promise::set_exception() is called,
throws the indicated exception.
Postcondition:false==valid()Throws:future_error with
error condition future_errc::no_state,
future_errc::broken_promise. Any exception passed
to promise::set_exception().
Member
function get_exception_ptr()
std::exception_ptrget_exception_ptr();Precondition:true==valid()Returns:
Waits until promise::set_value() or promise::set_exception() is
called. If set_value() is called, returns a default-constructed
std::exception_ptr. If set_exception()
is called, returns the passed std::exception_ptr.
Throws:future_error with
error condition future_errc::no_state.
Note:get_exception_ptr() does not invalidate
the shared_future. After calling get_exception_ptr(), you may still call shared_future::get().
Member
function wait()
voidwait();Effects:
Waits until promise::set_value() or promise::set_exception() is
called.
Throws:future_error with
error condition future_errc::no_state.
Templated
member function wait_for()
template<classRep,classPeriod>future_statuswait_for(std::chrono::duration<Rep,Period>const&timeout_duration)const;Effects:
Waits until promise::set_value() or promise::set_exception() is
called, or timeout_duration
has passed.
Result:
A future_status is
returned indicating the reason for returning.
Throws:future_error with
error condition future_errc::no_state
or timeout-related exceptions.
Templated
member function wait_until()
template<typenameClock,typenameDuration>future_statuswait_until(std::chrono::time_point<Clock,Duration>const&timeout_time)const;Effects:
Waits until promise::set_value() or promise::set_exception() is
called, or timeout_time
has passed.
Result:
A future_status is
returned indicating the reason for returning.
Throws:future_error with
error condition future_errc::no_state
or timeout-related exceptions.
Non-member function fibers::async()#include<boost/fiber/future/async.hpp>namespaceboost{namespacefibers{template<classFunction,class...Args>future<std::result_of_t<std::decay_t<Function>(std::decay_t<Args>...)>>async(Function&&fn,Args&&...args);template<classFunction,class...Args>future<std::result_of_t<std::decay_t<Function>(std::decay_t<Args>...)>>async(launchpolicy,Function&&fn,Args&&...args);template<typenameStackAllocator,classFunction,class...Args>future<std::result_of_t<std::decay_t<Function>(std::decay_t<Args>...)>>async(launchpolicy,std::allocator_arg_t,StackAllocatorsalloc,Function&&fn,Args&&...args);template<typenameStackAllocator,typenameAllocator,classFunction,class...Args>future<std::result_of_t<std::decay_t<Function>(std::decay_t<Args>...)>>async(launchpolicy,std::allocator_arg_t,StackAllocatorsalloc,Allocatoralloc,Function&&fn,Args&&...args);}}Effects:
Executes fn in a
fiber and returns an associated future<>.
Result:future<std::result_of_t<std::decay_t<Function>(std::decay_t<Args>...)>>
representing the shared state
associated with the asynchronous execution of fn.
Throws:fiber_error or future_error if an error occurs.
Notes:
The overloads accepting std::allocator_arg_t use the
passed StackAllocator
when constructing the launched fiber.
The overloads accepting launch use the passed launch when constructing the launched
fiber. The default
launch is post, as for the fiber constructor.
Deferred futures are not supported.
Template
promise<>
A promise<> provides a mechanism to store a value (or
exception) that can later be retrieved from the corresponding future<> object.
promise<>
and future<>
communicate via their underlying shared state.
#include<boost/fiber/future/promise.hpp>namespaceboost{namespacefibers{template<typenameR>classpromise{public:promise();template<typenameAllocator>promise(std::allocator_arg_t,Allocator);promise(promise&&)noexcept;promise&operator=(promise&&)noexcept;promise(promiseconst&)=delete;promise&operator=(promiseconst&)=delete;~promise();voidswap(promise&)noexcept;future<R>get_future();voidset_value(Rconst&);// member only of generic promise templatevoidset_value(R&&);// member only of generic promise templatevoidset_value(R&);// member only of promise< R & > templatevoidset_value();// member only of promise< void > templatevoidset_exception(std::exception_ptrp);};template<typenameR>voidswap(promise<R>&,promise<R>&)noexcept;}Default
constructor
promise();Effects:
Creates a promise with an empty shared
state.
Throws:
Exceptions caused by memory allocation.
Constructor
template<typenameAllocator>promise(std::allocator_arg_t,Allocatoralloc);Effects:
Creates a promise with an empty shared
state by using alloc.
Throws:
Exceptions caused by memory allocation.
See also:std::allocator_arg_tMove constructor
promise(promise&&other)noexcept;Effects:
Creates a promise by moving the shared
state from other.
Postcondition:other contains no
valid shared state.
Throws:
Nothing.
Destructor
~promise();Effects:
Destroys *this
and abandons the shared state
if shared state is ready; otherwise stores future_error
with error condition future_errc::broken_promise
as if by promise::set_exception(): the shared
state is set ready.
Member
function operator=()
promise&operator=(promise&&other)noexcept;Effects:
Transfers the ownership of shared state
to *this.
Postcondition:other contains no
valid shared state.
Throws:
Nothing.
Member function swap()
voidswap(promise&other)noexcept;Effects:
Swaps the shared state between
other and *this.
Throws:
Nothing.
Member
function get_future()
future<R>get_future();Returns:
A future<> with the same shared
state.
Throws:future_error with
future_errc::future_already_retrieved or future_errc::no_state.
Member function
set_value()
voidset_value(Rconst&value);// member only of generic promise templatevoidset_value(R&&value);// member only of generic promise templatevoidset_value(R&value);// member only of promise< R & > templatevoidset_value();// member only of promise< void > templateEffects:
Store the result in the shared state
and marks the state as ready.
Throws:future_error with
future_errc::future_already_satisfied or future_errc::no_state.
Member
function set_exception()
voidset_exception(std::exception_ptr);Effects:
Store an exception pointer in the shared
state and marks the state as ready.
Throws:future_error with
future_errc::future_already_satisfied or future_errc::no_state.
Non-member function
swap()template<typenameR>voidswap(promise<R>&l,promise<R>&r)noexcept;Effects:
Same as l.swap(r).
Template
packaged_task<>
A packaged_task<> wraps a callable target that
returns a value so that the return value can be computed asynchronously.
Conventional usage of packaged_task<> is like this:
Instantiate packaged_task<> with template arguments matching
the signature of the callable. Pass the callable to the constructor.
Call packaged_task::get_future() and capture
the returned future<> instance.
Launch a fiber to run the new packaged_task<>, passing any arguments required
by the original callable.
Call fiber::detach() on the newly-launched fiber.
At some later point, retrieve the result from the future<>.
This is, in fact, pretty much what fibers::async()
encapsulates.
#include<boost/fiber/future/packaged_task.hpp>namespaceboost{namespacefibers{template<classR,typename...Args>classpackaged_task<R(Args...)>{public:packaged_task()noexcept;template<typenameFn>explicitpackaged_task(Fn&&);template<typenameFn,typenameAllocator>packaged_task(std::allocator_arg_t,Allocatorconst&,Fn&&);packaged_task(packaged_task&&)noexcept;packaged_task&operator=(packaged_task&&)noexcept;packaged_task(packaged_taskconst&)=delete;packaged_task&operator=(packaged_taskconst&)=delete;~packaged_task();voidswap(packaged_task&)noexcept;boolvalid()constnoexcept;future<R>get_future();voidoperator()(Args...);voidreset();};template<typenameSignature>voidswap(packaged_task<Signature>&,packaged_task<Signature>&)noexcept;}}Default
constructor packaged_task()packaged_task()noexcept;Effects:
Constructs an object of class packaged_task
with no shared state.
Throws:
Nothing.
Templated
constructor packaged_task()template<typenameFn>explicitpackaged_task(Fn&&fn);template<typenameFn,typenameAllocator>packaged_task(std::allocator_arg_t,Allocatorconst&alloc,Fn&&fn);Effects:
Constructs an object of class packaged_task
with a shared state and copies
or moves the callable target fn
to internal storage.
Throws:
Exceptions caused by memory allocation.
Note:
The signature of Fn
should have a return type convertible to R.
See also:std::allocator_arg_tMove
constructor
packaged_task(packaged_task&&other)noexcept;Effects:
Creates a packaged_task by moving the shared
state from other.
Postcondition:other contains no
valid shared state.
Throws:
Nothing.
Destructor
~packaged_task();Effects:
Destroys *this
and abandons the shared state
if shared state is ready; otherwise stores future_error
with error condition future_errc::broken_promise
as if by promise::set_exception(): the shared
state is set ready.
Member
function operator=()
packaged_task&operator=(packaged_task&&other)noexcept;Effects:
Transfers the ownership of shared state
to *this.
Postcondition:other contains no
valid shared state.
Throws:
Nothing.
Member
function swap()
voidswap(packaged_task&other)noexcept;Effects:
Swaps the shared state between
other and *this.
Throws:
Nothing.
Member
function valid()
boolvalid()constnoexcept;Effects:
Returns true if *this
contains a shared state.
Throws:
Nothing.
Member
function get_future()
future<R>get_future();Returns:
A future<> with the same shared
state.
Throws:future_error with
future_errc::future_already_retrieved or future_errc::no_state.
Member
function operator()()
voidoperator()(Args&&...args);Effects:
Invokes the stored callable target. Any exception thrown by the callable
target fn is stored
in the shared state as if by
promise::set_exception(). Otherwise, the value
returned by fn is
stored in the shared state as if by promise::set_value().
Throws:future_error with
future_errc::no_state.
Member
function reset()
voidreset();Effects:
Resets the shared state and abandons
the result of previous executions. A new shared state is constructed.
Throws:future_error with
future_errc::no_state.
Non-member
function swap()template<typenameSignature>voidswap(packaged_task<Signature>&l,packaged_task<Signature>&r)noexcept;Effects:
Same as l.swap(r).
Fiber local storageSynopsis
Fiber local storage allows a separate instance of a given data item for each
fiber.
Cleanup
at fiber exit
When a fiber exits, the objects associated with each fiber_specific_ptr instance
are destroyed. By default, the object pointed to by a pointer p is destroyed by invoking deletep,
but this can be overridden for a specific instance of fiber_specific_ptr by
providing a cleanup routine func
to the constructor. In this case, the object is destroyed by invoking func(p). The cleanup functions are called in an unspecified
order.
Class
fiber_specific_ptr#include<boost/fiber/fss.hpp>namespaceboost{namespacefibers{template<typenameT>classfiber_specific_ptr{public:typedefTelement_type;fiber_specific_ptr();explicitfiber_specific_ptr(void(*fn)(T*));~fiber_specific_ptr();fiber_specific_ptr(fiber_specific_ptrconst&)=delete;fiber_specific_ptr&operator=(fiber_specific_ptrconst&)=delete;T*get()constnoexcept;T*operator->()constnoexcept;T&operator*()constnoexcept;T*release();voidreset(T*);};}}Constructor
fiber_specific_ptr();explicitfiber_specific_ptr(void(*fn)(T*));Requires:deletethis->get() is well-formed; fn(this->get()) does not throw
Effects:
Construct a fiber_specific_ptr object for storing
a pointer to an object of type T
specific to each fiber. When reset() is called, or the fiber exits, fiber_specific_ptr calls
fn(this->get()).
If the no-arguments constructor is used, the default delete-based
cleanup function will be used to destroy the fiber-local objects.
Throws:fiber_error if an error
occurs.
Destructor
~fiber_specific_ptr();Requires:
All the fiber specific instances associated to this fiber_specific_ptr
(except
maybe the one associated to this fiber) must be nullptr.
Effects:
Calls this->reset()
to clean up the associated value for the current fiber, and destroys
*this.
Remarks:
The requirement is an implementation restriction. If the destructor promised
to delete instances for all fibers, the implementation would be forced
to maintain a list of all the fibers having an associated specific ptr,
which is against the goal of fiber specific data. In general, a fiber_specific_ptr should
outlive the fibers that use it.
Care needs to be taken to ensure that any fibers still running after an instance
of fiber_specific_ptr has been destroyed do not call
any member functions on that instance.
Member
function get()
T*get()constnoexcept;Returns:
The pointer associated with the current fiber.
Throws:
Nothing.
The initial value associated with an instance of fiber_specific_ptr is
nullptr for each fiber.
Member
function operator->()
T*operator->()constnoexcept;Requires:this->get()
is not nullptr.
Returns:this->get()Throws:
Nothing.
Member
function operator*()
T&operator*()constnoexcept;Requires:this->get()
is not nullptr.
Returns:*(this->get())Throws:
Nothing.
Member
function release()
T*release();Effects:
Return this->get()
and store nullptr as the
pointer associated with the current fiber without invoking the cleanup
function.
Postcondition:this->get()==nullptrThrows:
Nothing.
Member
function reset()
voidreset(T*new_value);Effects:
If this->get()!=new_value and this->get() is not nullptr,
invoke deletethis->get() or fn(this->get()) as appropriate. Store new_value as the pointer associated
with the current fiber.
Postcondition:this->get()==new_valueThrows:
Exception raised during cleanup of previous value.
Migrating fibers
between threadsOverview
Each fiber owns a stack and manages its execution state, including all registers
and CPU flags, the instruction pointer and the stack pointer. That means, in
general, a fiber is not bound to a specific thread.
The main fiber on each thread, that is, the fiber on which
the thread is launched, cannot migrate to any other thread. Also Boost.Fiber implicitly creates a dispatcher fiber
for each thread — this cannot migrate either.
,
Of course it would be problematic to migrate a fiber that relies on thread-local storage.
Migrating a fiber from a logical CPU with heavy workload to another logical
CPU with a lighter workload might speed up the overall execution. Note that
in the case of NUMA-architectures, it is not always advisable to migrate data
between threads. Suppose fiber f is running on logical
CPU cpu0 which belongs to NUMA node node0.
The data of f are allocated on the physical memory located
at node0. Migrating the fiber from cpu0
to another logical CPU cpuX which is part of a different
NUMA node nodeX might reduce the performance of the application
due to increased latency of memory access.
Only fibers that are contained in algorithm’s ready queue can
migrate between threads. You cannot migrate a running fiber, nor one that is
blocked. You cannot migrate
a fiber if its context::is_context() method returns true for pinned_context.
In Boost.Fiber a fiber is migrated by invoking
context::detach() on the thread from which the fiber migrates
and context::attach() on the thread to which the fiber migrates.
Thus, fiber migration is accomplished by sharing state between instances of
a user-coded algorithm implementation running on different threads.
The fiber’s original thread calls algorithm::awakened(), passing
the fiber’s context*. The awakened() implementation calls context::detach().
At some later point, when the same or a different thread calls algorithm::pick_next(),
the pick_next()
implementation selects a ready fiber and calls context::attach() on
it before returning it.
As stated above, a context
for which is_context(pinned_context)==true
must never be passed to either context::detach() or context::attach().
It may only be returned from pick_next() called by the same thread
that passed that context to awakened().
Example
of work sharing
In the example work_sharing.cpp
multiple worker fibers are created on the main thread. Each fiber gets a character
as parameter at construction. This character is printed out ten times. Between
each iteration the fiber calls this_fiber::yield(). That puts
the fiber in the ready queue of the fiber-scheduler shared_ready_queue,
running in the current thread. The next fiber ready to be executed is dequeued
from the shared ready queue and resumed by shared_ready_queue
running on any participating thread.
All instances of shared_ready_queue share one global concurrent
queue, used as ready queue. This mechanism shares all worker fibers between
all instances of shared_ready_queue, thus between all
participating threads.
Setup
of threads and fibers
In main()
the fiber-scheduler is installed and the worker fibers and the threads are
launched.
boost::fibers::use_scheduling_algorithm<boost::fibers::algo::shared_work>();for(charc:std::string("abcdefghijklmnopqrstuvwxyz")){boost::fibers::fiber([c](){whatevah(c);}).detach();++fiber_count;}boost::fibers::detail::thread_barrierb(4);std::threadthreads[]={std::thread(thread,&b),std::thread(thread,&b),std::thread(thread,&b)};b.wait();{lock_typelk(mtx_count);cnd_count.wait(lk,[](){return0==fiber_count;});}BOOST_ASSERT(0==fiber_count);for(std::thread&t:threads){t.join();}
Install the scheduling algorithm boost::fibers::algo::shared_work
in the main thread too, so each new fiber gets launched into the shared
pool.
Launch a number of worker fibers; each worker fiber picks up a character
that is passed as parameter to fiber-function whatevah.
Each worker fiber gets detached.
Increment fiber counter for each new fiber.
Launch a couple of threads that join the work sharing.
sync with other threads: allow them to start processing
lock_type is typedef'ed
as std::unique_lock< std::mutex >
Suspend main fiber and resume worker fibers in the meanwhile. Main fiber
gets resumed (e.g returns from condition_variable_any::wait()) if all worker fibers are complete.
Releasing lock of mtx_count is required before joining the threads, otherwise
the other threads would be blocked inside condition_variable::wait() and
would never return (deadlock).
wait for threads to terminate
The start of the threads is synchronized with a barrier. The main fiber of
each thread (including main thread) is suspended until all worker fibers are
complete. When the main fiber returns from condition_variable::wait(),
the thread terminates: the main thread joins all other threads.
voidthread(boost::fibers::detail::thread_barrier*b){std::ostringstreambuffer;buffer<<"thread started "<<std::this_thread::get_id()<<std::endl;std::cout<<buffer.str()<<std::flush;boost::fibers::use_scheduling_algorithm<boost::fibers::algo::shared_work>();b->wait();lock_typelk(mtx_count);cnd_count.wait(lk,[](){return0==fiber_count;});BOOST_ASSERT(0==fiber_count);}
Install the scheduling algorithm boost::fibers::algo::shared_work
in order to join the work sharing.
sync with other threads: allow them to start processing
Suspend main fiber and resume worker fibers in the meanwhile. Main fiber
gets resumed (e.g returns from condition_variable_any::wait()) if all worker fibers are complete.
Each worker fiber executes function whatevah() with character me
as parameter. The fiber yields in a loop and prints out a message if it was
migrated to another thread.
voidwhatevah(charme){try{std::thread::idmy_thread=std::this_thread::get_id();{std::ostringstreambuffer;buffer<<"fiber "<<me<<" started on thread "<<my_thread<<'\n';std::cout<<buffer.str()<<std::flush;}for(unsignedi=0;i<10;++i){boost::this_fiber::yield();std::thread::idnew_thread=std::this_thread::get_id();if(new_thread!=my_thread){my_thread=new_thread;std::ostringstreambuffer;buffer<<"fiber "<<me<<" switched to thread "<<my_thread<<'\n';std::cout<<buffer.str()<<std::flush;}}}catch(...){}lock_typelk(mtx_count);if(0==--fiber_count){lk.unlock();cnd_count.notify_all();}}
get ID of initial thread
loop ten times
yield to other fibers
get ID of current thread
test if fiber was migrated to another thread
Decrement fiber counter for each completed fiber.
Notify all fibers waiting on cnd_count.
Scheduling
fibers
The fiber scheduler shared_ready_queue
is like round_robin, except
that it shares a common ready queue among all participating threads. A thread
participates in this pool by executing use_scheduling_algorithm()
before
any other Boost.Fiber operation.
The important point about the ready queue is that it’s a class static, common
to all instances of shared_ready_queue. Fibers that are enqueued via algorithm::awakened() (fibers
that are ready to be resumed) are thus available to all threads. It is required
to reserve a separate, scheduler-specific queue for the thread’s main fiber
and dispatcher fibers: these may not be shared between
threads! When we’re passed either of these fibers, push it there instead of
in the shared queue: it would be Bad News for thread B to retrieve and attempt
to execute thread A’s main fiber.
[awakened_ws]
When algorithm::pick_next() gets called inside one thread,
a fiber is dequeued from rqueue_ and will be resumed in
that thread.
[pick_next_ws]
The source code above is found in work_sharing.cpp.
Integrating Fibers
with Asynchronous CallbacksOverview
One of the primary benefits of Boost.Fiber
is the ability to use asynchronous operations for efficiency, while at the
same time structuring the calling code as if the operations
were synchronous. Asynchronous operations provide completion notification
in a variety of ways, but most involve a callback function of some kind.
This section discusses tactics for interfacing Boost.Fiber
with an arbitrary async operation.
For purposes of illustration, consider the following hypothetical API:
classAsyncAPI{public:// constructor acquires some resource that can be read and writtenAsyncAPI();// callbacks accept an int error code; 0 == successtypedefinterrorcode;// write callback only needs to indicate success or failuretemplate<typenameFn>voidinit_write(std::stringconst&data,Fn&&callback);// read callback needs to accept both errorcode and datatemplate<typenameFn>voidinit_read(Fn&&callback);// ... other operations ...};
The significant points about each of init_write() and init_read() are:
The AsyncAPI method only
initiates the operation. It returns immediately, while the requested
operation is still pending.
The method accepts a callback. When the operation completes, the callback
is called with relevant parameters (error code, data if applicable).
We would like to wrap these asynchronous methods in functions that appear
synchronous by blocking the calling fiber until the operation completes.
This lets us use the wrapper function’s return value to deliver relevant data.
promise<> and future<> are your friends
here.
Return Errorcode
The AsyncAPI::init_write()
callback passes only an errorcode.
If we simply want the blocking wrapper to return that errorcode,
this is an extremely straightforward use of promise<> and
future<>:
AsyncAPI::errorcodewrite_ec(AsyncAPI&api,std::stringconst&data){boost::fibers::promise<AsyncAPI::errorcode>promise;boost::fibers::future<AsyncAPI::errorcode>future(promise.get_future());// In general, even though we block waiting for future::get() and therefore// won't destroy 'promise' until promise::set_value() has been called, we// are advised that with threads it's possible for ~promise() to be// entered before promise::set_value() has returned. While that shouldn't// happen with fibers::promise, a robust way to deal with the lifespan// issue is to bind 'promise' into our lambda. Since promise is move-only,// use initialization capture.#if!defined(BOOST_NO_CXX14_INITIALIZED_LAMBDA_CAPTURES)api.init_write(data,[promise=std::move(promise)](AsyncAPI::errorcodeec)mutable{promise.set_value(ec);});#else// defined(BOOST_NO_CXX14_INITIALIZED_LAMBDA_CAPTURES)api.init_write(data,std::bind([](boost::fibers::promise<AsyncAPI::errorcode>&promise,AsyncAPI::errorcodeec){promise.set_value(ec);},std::move(promise),std::placeholders::_1));#endif// BOOST_NO_CXX14_INITIALIZED_LAMBDA_CAPTURESreturnfuture.get();}
All we have to do is:
Instantiate a promise<> of correct type.
Obtain its future<>.
Arrange for the callback to call promise::set_value().
Block on future::get().
This tactic for resuming a pending fiber works even if the callback is
called on a different thread than the one on which the initiating fiber
is running. In fact, the
example program’s dummy AsyncAPI
implementation illustrates that: it simulates async I/O by launching a
new thread that sleeps briefly and then calls the relevant callback.
Success or Exception
A wrapper more aligned with modern C++ practice would use an exception, rather
than an errorcode, to communicate
failure to its caller. This is straightforward to code in terms of write_ec():
voidwrite(AsyncAPI&api,std::stringconst&data){AsyncAPI::errorcodeec=write_ec(api,data);if(ec){throwmake_exception("write",ec);}}
The point is that since each fiber has its own stack, you need not repeat
messy boilerplate: normal encapsulation works.
Return Errorcode
or Data
Things get a bit more interesting when the async operation’s callback passes
multiple data items of interest. One approach would be to use std::pair<> to capture both:
std::pair<AsyncAPI::errorcode,std::string>read_ec(AsyncAPI&api){typedefstd::pair<AsyncAPI::errorcode,std::string>result_pair;boost::fibers::promise<result_pair>promise;boost::fibers::future<result_pair>future(promise.get_future());// We promise that both 'promise' and 'future' will survive until our// lambda has been called.#if!defined(BOOST_NO_CXX14_INITIALIZED_LAMBDA_CAPTURES)api.init_read([promise=std::move(promise)](AsyncAPI::errorcodeec,std::stringconst&data)mutable{promise.set_value(result_pair(ec,data));});#else// defined(BOOST_NO_CXX14_INITIALIZED_LAMBDA_CAPTURES)api.init_read(std::bind([](boost::fibers::promise<result_pair>&promise,AsyncAPI::errorcodeec,std::stringconst&data)mutable{promise.set_value(result_pair(ec,data));},std::move(promise),std::placeholders::_1,std::placeholders::_2));#endif// BOOST_NO_CXX14_INITIALIZED_LAMBDA_CAPTURESreturnfuture.get();}
Once you bundle the interesting data in std::pair<>, the code is effectively identical
to write_ec().
You can call it like this:
std::tie(ec,data)=read_ec(api);Data
or Exception
But a more natural API for a function that obtains data is to return only
the data on success, throwing an exception on error.
As with write()
above, it’s certainly possible to code a read() wrapper in terms of read_ec(). But since a given application is unlikely
to need both, let’s code read() from scratch, leveraging promise::set_exception():
std::stringread(AsyncAPI&api){boost::fibers::promise<std::string>promise;boost::fibers::future<std::string>future(promise.get_future());// Both 'promise' and 'future' will survive until our lambda has been// called.#if!defined(BOOST_NO_CXX14_INITIALIZED_LAMBDA_CAPTURES)api.init_read([&promise](AsyncAPI::errorcodeec,std::stringconst&data)mutable{if(!ec){promise.set_value(data);}else{promise.set_exception(std::make_exception_ptr(make_exception("read",ec)));}});#else// defined(BOOST_NO_CXX14_INITIALIZED_LAMBDA_CAPTURES)api.init_read(std::bind([](boost::fibers::promise<std::string>&promise,AsyncAPI::errorcodeec,std::stringconst&data)mutable{if(!ec){promise.set_value(data);}else{promise.set_exception(std::make_exception_ptr(make_exception("read",ec)));}},std::move(promise),std::placeholders::_1,std::placeholders::_2));#endif// BOOST_NO_CXX14_INITIALIZED_LAMBDA_CAPTURESreturnfuture.get();}future::get() will do the right thing, either returning std::string
or throwing an exception.
Success/Error
Virtual Methods
One classic approach to completion notification is to define an abstract
base class with success()
and error()
methods. Code wishing to perform async I/O must derive a subclass, override
each of these methods and pass the async operation a pointer to a subclass
instance. The abstract base class might look like this:
// every async operation receives a subclass instance of this abstract base// class through which to communicate its resultstructResponse{typedefstd::shared_ptr<Response>ptr;// called if the operation succeedsvirtualvoidsuccess(std::stringconst&data)=0;// called if the operation failsvirtualvoiderror(AsyncAPIBase::errorcodeec)=0;};
Now the AsyncAPI operation
might look more like this:
// derive Response subclass, instantiate, pass Response::ptrvoidinit_read(Response::ptr);
We can address this by writing a one-size-fits-all PromiseResponse:
classPromiseResponse:publicResponse{public:// called if the operation succeedsvirtualvoidsuccess(std::stringconst&data){promise_.set_value(data);}// called if the operation failsvirtualvoiderror(AsyncAPIBase::errorcodeec){promise_.set_exception(std::make_exception_ptr(make_exception("read",ec)));}boost::fibers::future<std::string>get_future(){returnpromise_.get_future();}private:boost::fibers::promise<std::string>promise_;};
Now we can simply obtain the future<> from that PromiseResponse
and wait on its get():
std::stringread(AsyncAPI&api){// Because init_read() requires a shared_ptr, we must allocate our// ResponsePromise on the heap, even though we know its lifespan.autopromisep(std::make_shared<PromiseResponse>());boost::fibers::future<std::string>future(promisep->get_future());// Both 'promisep' and 'future' will survive until our lambda has been// called.api.init_read(promisep);returnfuture.get();}
The source code above is found in adapt_callbacks.cpp
and adapt_method_calls.cpp.
Then
There’s Boost.Asio
Since the simplest form of Boost.Asio asynchronous operation completion token
is a callback function, we could apply the same tactics for Asio as for our
hypothetical AsyncAPI asynchronous
operations.
Fortunately we need not. Boost.Asio incorporates a mechanism
This mechanism has been proposed as a conventional way to allow the caller
of an arbitrary async function to specify completion handling: N4045.
by which the caller can customize the notification behavior of
any async operation. Therefore we can construct a completion token
which, when passed to a Boost.Asio
async operation, requests blocking for the calling fiber.
A typical Asio async function might look something like this:
per N4045template<...,classCompletionToken>deduced_return_typeasync_something(...,CompletionToken&&token){// construct handler_type instance from CompletionTokenhandler_type<CompletionToken,...>::typehandler(token);// construct async_result instance from handler_typeasync_result<decltype(handler)>result(handler);// ... arrange to call handler on completion ...// ... initiate actual I/O operation ...returnresult.get();}
We will engage that mechanism, which is based on specializing Asio’s handler_type<>
template for the CompletionToken
type and the signature of the specific callback. The remainder of this discussion
will refer back to async_something() as the Asio async function under consideration.
The implementation described below uses lower-level facilities than promise and future
because the promise mechanism
interacts badly with io_service::stop().
It produces broken_promise
exceptions.
boost::fibers::asio::yield is a completion token of this kind.
yield is an instance of
yield_t:
classyield_t{public:yield_t()=default;/**
* @code
* static yield_t yield;
* boost::system::error_code myec;
* func(yield[myec]);
* @endcode
* @c yield[myec] returns an instance of @c yield_t whose @c ec_ points
* to @c myec. The expression @c yield[myec] "binds" @c myec to that
* (anonymous) @c yield_t instance, instructing @c func() to store any
* @c error_code it might produce into @c myec rather than throwing @c
* boost::system::system_error.
*/yield_toperator[](boost::system::error_code&ec)const{yield_ttmp;tmp.ec_=&ec;returntmp;}//private:// ptr to bound error_code instance if anyboost::system::error_code*ec_{nullptr};};yield_t is in fact only a
placeholder, a way to trigger Boost.Asio customization. It can bind a boost::system::error_code for use by the actual
handler.
yield is declared as:
// canonical instancethread_localyield_tyield{};
Asio customization is engaged by specializing boost::asio::handler_type<>
for yield_t:
// Handler type specialisation for fibers::asio::yield.// When 'yield' is passed as a completion handler which accepts only// error_code, use yield_handler<void>. yield_handler will take care of the// error_code one way or another.template<typenameReturnType>structhandler_type<fibers::asio::yield_t,ReturnType(boost::system::error_code)>{typedeffibers::asio::detail::yield_handler<void>type;};
(There are actually four different specializations in detail/yield.hpp,
one for each of the four Asio async callback signatures we expect.)
The above directs Asio to use yield_handler
as the actual handler for an async operation to which yield
is passed. There’s a generic yield_handler<T>
implementation and a yield_handler<void>
specialization. Let’s start with the <void> specialization:
// yield_handler<void> is like yield_handler<T> without value_. In fact it's// just like yield_handler_base.template<>classyield_handler<void>:publicyield_handler_base{public:explicityield_handler(yield_tconst&y):yield_handler_base{y}{}// nullary completion callbackvoidoperator()(){(*this)(boost::system::error_code());}// inherit operator()(error_code) overload from base classusingyield_handler_base::operator();};async_something(),
having consulted the handler_type<> traits specialization, instantiates
a yield_handler<void> to
be passed as the actual callback for the async operation. yield_handler’s
constructor accepts the yield_t
instance (the yield object
passed to the async function) and passes it along to yield_handler_base:
// This class encapsulates common elements between yield_handler<T> (capturing// a value to return from asio async function) and yield_handler<void> (no// such value). See yield_handler<T> and its <void> specialization below. Both// yield_handler<T> and yield_handler<void> are passed by value through// various layers of asio functions. In other words, they're potentially// copied multiple times. So key data such as the yield_completion instance// must be stored in our async_result<yield_handler<>> specialization, which// should be instantiated only once.classyield_handler_base{public:yield_handler_base(yield_tconst&y):// capture the context* associated with the running fiberctx_{boost::fibers::context::active()},// capture the passed yield_tyt_(y){}// completion callback passing only (error_code)voidoperator()(boost::system::error_codeconst&ec){BOOST_ASSERT_MSG(ycomp_,"Must inject yield_completion* ""before calling yield_handler_base::operator()()");BOOST_ASSERT_MSG(yt_.ec_,"Must inject boost::system::error_code* ""before calling yield_handler_base::operator()()");// If originating fiber is busy testing state_ flag, wait until it// has observed (completed != state_).yield_completion::lock_tlk{ycomp_->mtx_};yield_completion::state_tstate=ycomp_->state_;// Notify a subsequent yield_completion::wait() call that it need not// suspend.ycomp_->state_=yield_completion::complete;// set the error_code bound by yield_t*yt_.ec_=ec;// unlock the lock that protects state_lk.unlock();// If ctx_ is still active, e.g. because the async operation// immediately called its callback (this method!) before the asio// async function called async_result_base::get(), we must not set it// ready.if(yield_completion::waiting==state){// wake the fiberfibers::context::active()->schedule(ctx_);}}//private:boost::fibers::context*ctx_;yield_tyt_;// We depend on this pointer to yield_completion, which will be injected// by async_result.yield_completion::ptr_tycomp_{};};yield_handler_base stores
a copy of the yield_t instance
— which, as shown above, contains only an error_code*. It also captures the context*
for the currently-running fiber by calling context::active().
You will notice that yield_handler_base
has one more data member (ycomp_)
that is initialized to nullptr
by its constructor — though its operator()() method relies on ycomp_
being non-null. More on this in a moment.
Having constructed the yield_handler<void>
instance, async_something() goes on to construct an async_result
specialized for the handler_type<>::type:
in this case, async_result<yield_handler<void>>.
It passes the yield_handler<void>
instance to the new async_result
instance.
// Without the need to handle a passed value, our yield_handler<void>// specialization is just like async_result_base.template<>classasync_result<boost::fibers::asio::detail::yield_handler<void>>:publicboost::fibers::asio::detail::async_result_base{public:typedefvoidtype;explicitasync_result(boost::fibers::asio::detail::yield_handler<void>&h):boost::fibers::asio::detail::async_result_base{h}{}};
Naturally that leads us straight to async_result_base:
// Factor out commonality between async_result<yield_handler<T>> and// async_result<yield_handler<void>>classasync_result_base{public:explicitasync_result_base(yield_handler_base&h):ycomp_{newyield_completion{}}{// Inject ptr to our yield_completion instance into this// yield_handler<>.h.ycomp_=this->ycomp_;// if yield_t didn't bind an error_code, make yield_handler_base's// error_code* point to an error_code local to this object so// yield_handler_base::operator() can unconditionally store through// its error_code*if(!h.yt_.ec_){h.yt_.ec_=&ec_;}}voidget(){// Unless yield_handler_base::operator() has already been called,// suspend the calling fiber until that call.ycomp_->wait();// The only way our own ec_ member could have a non-default value is// if our yield_handler did not have a bound error_code AND the// completion callback passed a non-default error_code.if(ec_){throw_exception(boost::system::system_error{ec_});}}private:// If yield_t does not bind an error_code instance, store into here.boost::system::error_codeec_{};yield_completion::ptr_tycomp_;};
This is how yield_handler_base::ycomp_
becomes non-null: async_result_base’s
constructor injects a pointer back to its own yield_completion
member.
Recall that the canonical yield_t
instance yield initializes
its error_code*
member ec_ to nullptr. If this instance is passed to async_something()
(ec_ is still nullptr), the copy stored in yield_handler_base will likewise have null
ec_. async_result_base’s
constructor sets yield_handler_base’s
yield_t’s ec_
member to point to its own error_code
member.
The stage is now set. async_something() initiates the actual async operation, arranging
to call its yield_handler<void>
instance on completion. Let’s say, for the sake of argument, that the actual
async operation’s callback has signature void(error_code).
But since it’s an async operation, control returns at once to async_something().
async_something()
calls async_result<yield_handler<void>>::get(),
and will return its return value.
async_result<yield_handler<void>>::get() inherits
async_result_base::get().
async_result_base::get() immediately
calls yield_completion::wait().
// Bundle a completion bool flag with a spinlock to protect it.structyield_completion{enumstate_t{init,waiting,complete};typedeffibers::detail::spinlockmutex_t;typedefstd::unique_lock<mutex_t>lock_t;typedefboost::intrusive_ptr<yield_completion>ptr_t;std::atomic<std::size_t>use_count_{0};mutex_tmtx_{};state_tstate_{init};voidwait(){// yield_handler_base::operator()() will set state_ `complete` and// attempt to wake a suspended fiber. It would be Bad if that call// happened between our detecting (complete != state_) and suspending.lock_tlk{mtx_};// If state_ is already set, we're done here: don't suspend.if(complete!=state_){state_=waiting;// suspend(unique_lock<spinlock>) unlocks the lock in the act of// resuming another fiberfibers::context::active()->suspend(lk);}}friendvoidintrusive_ptr_add_ref(yield_completion*yc)noexcept{BOOST_ASSERT(nullptr!=yc);yc->use_count_.fetch_add(1,std::memory_order_relaxed);}friendvoidintrusive_ptr_release(yield_completion*yc)noexcept{BOOST_ASSERT(nullptr!=yc);if(1==yc->use_count_.fetch_sub(1,std::memory_order_release)){std::atomic_thread_fence(std::memory_order_acquire);deleteyc;}}};
Supposing that the pending async operation has not yet completed, yield_completion::completed_ will still be false, and wait() will call context::suspend() on
the currently-running fiber.
Other fibers will now have a chance to run.
Some time later, the async operation completes. It calls yield_handler<void>::operator()(error_codeconst&) with an error_code
indicating either success or failure. We’ll consider both cases.
yield_handler<void> explicitly
inherits operator()(error_codeconst&) from yield_handler_base.
yield_handler_base::operator()(error_codeconst&) first sets yield_completion::completed_true. This way, if async_something()’s
async operation completes immediately — if yield_handler_base::operator() is called even before async_result_base::get()
— the calling fiber will not suspend.
The actual error_code produced
by the async operation is then stored through the stored yield_t::ec_ pointer.
If async_something()’s
caller used (e.g.) yield[my_ec] to bind a local error_code
instance, the actual error_code
value is stored into the caller’s variable. Otherwise, it is stored into
async_result_base::ec_.
If the stored fiber context yield_handler_base::ctx_
is not already running, it is marked as ready to run by passing it to context::schedule().
Control then returns from yield_handler_base::operator(): the callback is done.
In due course, that fiber is resumed. Control returns from context::suspend() to
yield_completion::wait(),
which returns to async_result_base::get().
If the original caller passed yield[my_ec] to async_something() to bind a local error_code
instance, then yield_handler_base::operator() stored its error_code
to the caller’s my_ec
instance, leaving async_result_base::ec_
initialized to success.
If the original caller passed yield
to async_something()
without binding a local error_code
variable, then yield_handler_base::operator() stored its error_code
into async_result_base::ec_.
If in fact that error_code
is success, then all is well.
Otherwise — the original caller did not bind a local error_code
and yield_handler_base::operator() was called with an error_code
indicating error — async_result_base::get() throws system_error
with that error_code.
The case in which async_something()’s completion callback has signature void() is
similar. yield_handler<void>::operator()()
invokes the machinery above with a successerror_code.
A completion callback with signature void(error_code,T)
(that is: in addition to error_code,
callback receives some data item) is handled somewhat differently. For this
kind of signature, handler_type<>::type
specifies yield_handler<T> (for
T other than void).
A yield_handler<T> reserves
a value_ pointer to a value
of type T:
// asio uses handler_type<completion token type, signature>::type to decide// what to instantiate as the actual handler. Below, we specialize// handler_type< yield_t, ... > to indicate yield_handler<>. So when you pass// an instance of yield_t as an asio completion token, asio selects// yield_handler<> as the actual handler class.template<typenameT>classyield_handler:publicyield_handler_base{public:// asio passes the completion token to the handler constructorexplicityield_handler(yield_tconst&y):yield_handler_base{y}{}// completion callback passing only value (T)voidoperator()(Tt){// just like callback passing success error_code(*this)(boost::system::error_code(),std::move(t));}// completion callback passing (error_code, T)voidoperator()(boost::system::error_codeconst&ec,Tt){BOOST_ASSERT_MSG(value_,"Must inject value ptr ""before caling yield_handler<T>::operator()()");// move the value to async_result<> instance BEFORE waking up a// suspended fiber*value_=std::move(t);// forward the call to base-class completion handleryield_handler_base::operator()(ec);}//private:// pointer to destination for eventual value// this must be injected by async_result before operator()() is calledT*value_{nullptr};};
This pointer is initialized to nullptr.
When async_something()
instantiates async_result<yield_handler<T>>:
// asio constructs an async_result<> instance from the yield_handler specified// by handler_type<>::type. A particular asio async method constructs the// yield_handler, constructs this async_result specialization from it, then// returns the result of calling its get() method.template<typenameT>classasync_result<boost::fibers::asio::detail::yield_handler<T>>:publicboost::fibers::asio::detail::async_result_base{public:// type returned by get()typedefTtype;explicitasync_result(boost::fibers::asio::detail::yield_handler<T>&h):boost::fibers::asio::detail::async_result_base{h}{// Inject ptr to our value_ member into yield_handler<>: result will// be stored here.h.value_=&value_;}// asio async method returns result of calling get()typeget(){boost::fibers::asio::detail::async_result_base::get();returnstd::move(value_);}private:typevalue_{};};
this async_result<>
specialization reserves a member of type T
to receive the passed data item, and sets yield_handler<T>::value_ to point to its own data member.
async_result<yield_handler<T>>
overrides get().
The override calls async_result_base::get(),
so the calling fiber suspends as described above.
yield_handler<T>::operator()(error_code,T) stores
its passed T value into
async_result<yield_handler<T>>::value_.
Then it passes control to yield_handler_base::operator()(error_code) to deal with waking the original fiber as
described above.
When async_result<yield_handler<T>>::get() resumes,
it returns the stored value_
to async_something()
and ultimately to async_something()’s caller.
The case of a callback signature void(T)
is handled by having yield_handler<T>::operator()(T) engage
the void(error_code,T) machinery,
passing a successerror_code.
The source code above is found in yield.hpp
and detail/yield.hpp.
Integrating
Fibers with Nonblocking I/OOverview
Nonblocking I/O is distinct from asynchronous
I/O. A true async I/O operation promises to initiate the operation and notify
the caller on completion, usually via some sort of callback (as described in
Integrating Fibers with Asynchronous Callbacks).
In contrast, a nonblocking I/O operation refuses to start at all if it would
be necessary to block, returning an error code such as EWOULDBLOCK. The operation is performed
only when it can complete immediately. In effect, the caller must repeatedly
retry the operation until it stops returning EWOULDBLOCK.
In a classic event-driven program, it can be something of a headache to use
nonblocking I/O. At the point where the nonblocking I/O is attempted, a return
value of EWOULDBLOCK requires
the caller to pass control back to the main event loop, arranging to retry
again on the next iteration.
Worse, a nonblocking I/O operation might partially succeed.
That means that the relevant business logic must continue receiving control
on every main loop iteration until all required data have been processed: a
doubly-nested loop, implemented as a callback-driven state machine.
Boost.Fiber can simplify this problem immensely.
Once you have integrated with the application's main loop as described in
Sharing a Thread with Another Main Loop,
waiting for the next main-loop iteration is as simple as calling this_fiber::yield().
Example
Nonblocking API
For purposes of illustration, consider this API:
classNonblockingAPI{public:NonblockingAPI();// nonblocking operation: may return EWOULDBLOCKintread(std::string&data,std::size_tdesired);...};Polling
for Completion
We can build a low-level wrapper around NonblockingAPI::read()
that shields its caller from ever having to deal with EWOULDBLOCK:
// guaranteed not to return EWOULDBLOCKintread_chunk(NonblockingAPI&api,std::string&data,std::size_tdesired){interror;while(EWOULDBLOCK==(error=api.read(data,desired))){// not ready yet -- try again on the next iteration of the// application's main loopboost::this_fiber::yield();}returnerror;}Filling
All Desired Data
Given read_chunk(),
we can straightforwardly iterate until we have all desired data:
// keep reading until desired length, EOF or error// may return both partial data and nonzero errorintread_desired(NonblockingAPI&api,std::string&data,std::size_tdesired){// we're going to accumulate results into 'data'data.clear();std::stringchunk;interror=0;while(data.length()<desired&&(error=read_chunk(api,chunk,desired-data.length()))==0){data.append(chunk);}returnerror;}
(Of course there are more efficient ways to accumulate
string data. That's not the point of this example.)
Wrapping
it Up
Finally, we can define a relevant exception:
// exception class augmented with both partially-read data and errorcodeclassIncompleteRead:publicstd::runtime_error{public:IncompleteRead(std::stringconst&what,std::stringconst&partial,intec):std::runtime_error(what),partial_(partial),ec_(ec){}std::stringget_partial()const{returnpartial_;}intget_errorcode()const{returnec_;}private:std::stringpartial_;intec_;};
and write a simple read()
function that either returns all desired data or throws IncompleteRead:
// read all desired data or throw IncompleteReadstd::stringread(NonblockingAPI&api,std::size_tdesired){std::stringdata;intec(read_desired(api,data,desired));// for present purposes, EOF isn't a failureif(0==ec||EOF==ec){returndata;}// oh oh, partial readstd::ostringstreammsg;msg<<"NonblockingAPI::read() error "<<ec<<" after "<<data.length()<<" of "<<desired<<" characters";throwIncompleteRead(msg.str(),data,ec);}
Once we can transparently wait for the next main-loop iteration using this_fiber::yield(),
ordinary encapsulation Just Works.
The source code above is found in adapt_nonblocking.cpp.
when_any / when_all
functionalityOverview
A bit of wisdom from the early days of computing still holds true today: prefer
to model program state using the instruction pointer rather than with Boolean
flags. In other words, if the program must do something and
then do something almost the same, but with minor changes... perhaps parts
of that something should be broken out as smaller separate functions, rather
than introducing flags to alter the internal behavior of a monolithic function.
To that we would add: prefer to describe control flow using C++ native constructs
such as function calls, if, while, for,
do et al. rather than as chains
of callbacks.
One of the great strengths of Boost.Fiber
is the flexibility it confers on the coder to restructure an application from
chains of callbacks to straightforward C++ statement sequence, even when code
in that fiber is in fact interleaved with code running in other fibers.
There has been much recent discussion about the benefits of when_any and when_all
functionality. When dealing with asynchronous and possibly unreliable services,
these are valuable idioms. But of course when_any and when_all are closely
tied to the use of chains of callbacks.
This section presents recipes for achieving the same ends, in the context of
a fiber that wants to do something when one or more other independent
activities have completed. Accordingly, these are wait_something() functions rather than when_something() functions. The expectation is that the calling
fiber asks to launch those independent activities, then waits for them, then
sequentially proceeds with whatever processing depends on those results.
The function names shown (e.g. wait_first_simple())
are for illustrative purposes only, because all these functions have been bundled
into a single source file. Presumably, if (say) wait_first_success()
best suits your application needs, you could introduce that variant with the
name wait_any().
The functions presented in this section accept variadic argument lists of
task functions. Corresponding wait_something() functions accepting a container of task
functions are left as an exercise for the interested reader. Those should
actually be simpler. Most of the complexity would arise from overloading
the same name for both purposes.
All the source code for this section is found in wait_stuff.cpp.
Example
Task Function
We found it convenient to model an asynchronous
task using this function:
template<typenameT>Tsleeper_impl(Titem,intms,boolthrw=false){std::ostringstreamdescb,funcb;descb<<item;std::stringdesc(descb.str());funcb<<" sleeper("<<item<<")";Verbosev(funcb.str());boost::this_fiber::sleep_for(std::chrono::milliseconds(ms));if(thrw){throwstd::runtime_error(desc);}returnitem;}
with type-specific sleeper()front ends for std::string,
double and int.
Verbose simply prints a message
to std::cout on construction and destruction.
Basically:
sleeper()
prints a start message;
sleeps for the specified number of milliseconds;
if thrw is passed as true, throws a string description of the
passed item;
else returns the passed item.
On the way out, sleeper() produces a stop message.
This function will feature in the example calls to the various functions presented
below.
when_anywhen_any,
simple completion
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 flagstructDone{private:boost::fibers::condition_variablecond;boost::fibers::mutexmutex;boolready=false;public:typedefstd::shared_ptr<Done>ptr;voidwait(){std::unique_lock<boost::fibers::mutex>lock(mutex);cond.wait(lock,[this](){returnready;});}voidnotify(){{std::unique_lock<boost::fibers::mutex>lock(mutex);ready=true;}// release mutexcond.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>voidwait_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.autodone(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.voidwait_first_simple_impl(Done::ptr){}// When there's at least one function to wait for, launch it and recur to// process the rest.template<typenameFn,typename...Fns>voidwait_first_simple_impl(Done::ptrdone,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.
when_any,
return value
It seems more useful to add the ability to capture the return value from
the first of the task functions to complete. Again, we assume that none
will throw an exception.
One tactic would be to adapt our Done class to store the first
of the return values, rather than a simple bool.
However, we choose instead to use a buffered_channel<>.
We'll only need to enqueue the first value, so we'll buffered_channel::close() it
once we've retrieved that value. Subsequent push() calls will return closed.
// Assume that all passed functions have the same return type. The return type// of wait_first_value() is the return type of the first passed function. It is// simply invalid to pass NO functions.template<typenameFn,typename...Fns>typenamestd::result_of<Fn()>::typewait_first_value(Fn&&function,Fns&&...functions){typedeftypenamestd::result_of<Fn()>::typereturn_t;typedefboost::fibers::buffered_channel<return_t>channel_t;autochanp(std::make_shared<channel_t>(64));// launch all the relevant fiberswait_first_value_impl<return_t>(chanp,std::forward<Fn>(function),std::forward<Fns>(functions)...);// retrieve the first valuereturn_tvalue(chanp->value_pop());// close the channel: no subsequent push() has to succeedchanp->close();returnvalue;}The meat of the wait_first_value_impl() function is as you might expect:
template<typenameT,typenameFn>voidwait_first_value_impl(std::shared_ptr<boost::fibers::buffered_channel<T>>chan,Fn&&function){boost::fibers::fiber([chan,function](){// Ignore channel_op_status returned by push():// might be closed; we simply don't care.chan->push(function());}).detach();}
It calls the passed function, pushes its return value and ignores the
push()
result. You might call it like this:
std::stringresult=wait_first_value([](){returnsleeper("wfv_third",150);},[](){returnsleeper("wfv_second",100);},[](){returnsleeper("wfv_first",50);});std::cout<<"wait_first_value() => "<<result<<std::endl;assert(result=="wfv_first");when_any,
produce first outcome, whether result or exception
We may not be running in an environment in which we can guarantee no exception
will be thrown by any of our task functions. In that case, the above implementations
of wait_first_something() would be naïve: as mentioned in the section on Fiber Management, an uncaught
exception in one of our task fibers would cause std::terminate() to be called.
Let's at least ensure that such an exception would propagate to the fiber
awaiting the first result. We can use future<> to transport
either a return value or an exception. Therefore, we will change wait_first_value()'s buffered_channel<> to
hold future<T>
items instead of simply T.
Once we have a future<>
in hand, all we need do is call future::get(), which will either
return the value or rethrow the exception.
template<typenameFn,typename...Fns>typenamestd::result_of<Fn()>::typewait_first_outcome(Fn&&function,Fns&&...functions){// In this case, the value we pass through the channel is actually a// future -- which is already ready. future can carry either a value or an// exception.typedeftypenamestd::result_of<Fn()>::typereturn_t;typedefboost::fibers::future<return_t>future_t;typedefboost::fibers::buffered_channel<future_t>channel_t;autochanp(std::make_shared<channel_t>(64));// launch all the relevant fiberswait_first_outcome_impl<return_t>(chanp,std::forward<Fn>(function),std::forward<Fns>(functions)...);// retrieve the first futurefuture_tfuture(chanp->value_pop());// close the channel: no subsequent push() has to succeedchanp->close();// either return value or throw exceptionreturnfuture.get();}
So far so good — but there's a timing issue. How should we obtain the future<>
to buffered_channel::push() on the queue?
We could call fibers::async(). That would certainly produce
a future<>
for the task function. The trouble is that it would return too quickly!
We only want future<>
items for completed tasks on our queue<>. In fact, we only want the future<>
for the one that completes first. If each fiber launched by wait_first_outcome()
were to push()
the result of calling async(), the queue would only ever report the
result of the leftmost task item — not the one that
completes most quickly.
Calling future::get() on the future returned by async()
wouldn't be right. You can only call get() once per future<> instance! And if there were an
exception, it would be rethrown inside the helper fiber at the producer
end of the queue, rather than propagated to the consumer end.
We could call future::wait(). That would block the helper fiber
until the future<>
became ready, at which point we could push() it to be retrieved by wait_first_outcome().
That would work — but there's a simpler tactic that avoids creating an extra
fiber. We can wrap the task function in a packaged_task<>.
While one naturally thinks of passing a packaged_task<> to a new fiber — that is, in fact,
what async()
does — in this case, we're already running in the helper fiber at the producer
end of the queue! We can simply call the packaged_task<>.
On return from that call, the task function has completed, meaning that
the future<>
obtained from the packaged_task<> is certain to be ready. At that
point we can simply push() it to the queue.
template<typenameT,typenameCHANP,typenameFn>voidwait_first_outcome_impl(CHANPchan,Fn&&function){boost::fibers::fiber(// Use std::bind() here for C++11 compatibility. C++11 lambda capture// can't move a move-only Fn type, but bind() can. Let bind() move the// channel pointer and the function into the bound object, passing// references into the lambda.std::bind([](CHANP&chan,typenamestd::decay<Fn>::type&function){// Instantiate a packaged_task to capture any exception thrown by// function.boost::fibers::packaged_task<T()>task(function);// Immediately run this packaged_task on same fiber. We want// function() to have completed BEFORE we push the future.task();// Pass the corresponding future to consumer. Ignore// channel_op_status returned by push(): might be closed; we// simply don't care.chan->push(task.get_future());},chan,std::forward<Fn>(function))).detach();}
Calling it might look like this:
std::stringresult=wait_first_outcome([](){returnsleeper("wfos_first",50);},[](){returnsleeper("wfos_second",100);},[](){returnsleeper("wfos_third",150);});std::cout<<"wait_first_outcome(success) => "<<result<<std::endl;assert(result=="wfos_first");std::stringthrown;try{result=wait_first_outcome([](){returnsleeper("wfof_first",50,true);},[](){returnsleeper("wfof_second",100);},[](){returnsleeper("wfof_third",150);});}catch(std::exceptionconst&e){thrown=e.what();}std::cout<<"wait_first_outcome(fail) threw '"<<thrown<<"'"<<std::endl;assert(thrown=="wfof_first");when_any,
produce first success
One scenario for when_any functionality is when we're redundantly
contacting some number of possibly-unreliable web services. Not only might
they be slow — any one of them might produce a failure rather than the desired
result.
In such a case, wait_first_outcome() isn't the right approach. If one
of the services produces an error quickly, while another follows up with
a real answer, we don't want to prefer the error just because it arrived
first!
Given the queue<future<T>> we already constructed for
wait_first_outcome(),
though, we can readily recast the interface function to deliver the first
successful result.
That does beg the question: what if all the task functions
throw an exception? In that case we'd probably better know about it.
The C++
Parallelism Draft Technical Specification proposes a std::exception_list exception capable of delivering
a collection of std::exception_ptrs. Until that becomes universally
available, let's fake up an exception_list
of our own:
classexception_list:publicstd::runtime_error{public:exception_list(std::stringconst&what):std::runtime_error(what){}typedefstd::vector<std::exception_ptr>bundle_t;// N4407 proposed std::exception_list APItypedefbundle_t::const_iteratoriterator;std::size_tsize()constnoexcept{returnbundle_.size();}iteratorbegin()constnoexcept{returnbundle_.begin();}iteratorend()constnoexcept{returnbundle_.end();}// extension to populatevoidadd(std::exception_ptrep){bundle_.push_back(ep);}private:bundle_tbundle_;};
Now we can build wait_first_success(), using wait_first_outcome_impl().
Instead of retrieving only the first future<> from the queue, we must now loop
over future<>
items. Of course we must limit that iteration! If we launch only count producer fibers, the (count+1)stbuffered_channel::pop() call
would block forever.
Given a ready future<>,
we can distinguish failure by calling future::get_exception_ptr().
If the future<>
in fact contains a result rather than an exception, get_exception_ptr() returns nullptr.
In that case, we can confidently call future::get() to return
that result to our caller.
If the std::exception_ptr is notnullptr, though, we collect
it into our pending exception_list
and loop back for the next future<> from the queue.
If we fall out of the loop — if every single task fiber threw an exception
— we throw the exception_list
exception into which we've been collecting those std::exception_ptrs.
template<typenameFn,typename...Fns>typenamestd::result_of<Fn()>::typewait_first_success(Fn&&function,Fns&&...functions){std::size_tcount(1+sizeof...(functions));// In this case, the value we pass through the channel is actually a// future -- which is already ready. future can carry either a value or an// exception.typedeftypenamestd::result_of<typenamestd::decay<Fn>::type()>::typereturn_t;typedefboost::fibers::future<return_t>future_t;typedefboost::fibers::buffered_channel<future_t>channel_t;autochanp(std::make_shared<channel_t>(64));// launch all the relevant fiberswait_first_outcome_impl<return_t>(chanp,std::forward<Fn>(function),std::forward<Fns>(functions)...);// instantiate exception_list, just in caseexception_listexceptions("wait_first_success() produced only errors");// retrieve up to 'count' results -- but stop there!for(std::size_ti=0;i<count;++i){// retrieve the next futurefuture_tfuture(chanp->value_pop());// retrieve exception_ptr if anystd::exception_ptrerror(future.get_exception_ptr());// if no error, then yay, return valueif(!error){// close the channel: no subsequent push() has to succeedchanp->close();// show caller the value we gotreturnfuture.get();}// error is non-null: collectexceptions.add(error);}// We only arrive here when every passed function threw an exception.// Throw our collection to inform caller.throwexceptions;}
A call might look like this:
std::stringresult=wait_first_success([](){returnsleeper("wfss_first",50,true);},[](){returnsleeper("wfss_second",100);},[](){returnsleeper("wfss_third",150);});std::cout<<"wait_first_success(success) => "<<result<<std::endl;assert(result=="wfss_second");when_any,
heterogeneous types
We would be remiss to ignore the case in which the various task functions
have distinct return types. That means that the value returned by the first
of them might have any one of those types. We can express that with Boost.Variant.
To keep the example simple, we'll revert to pretending that none of them
can throw an exception. That makes wait_first_value_het() strongly resemble wait_first_value().
We can actually reuse wait_first_value_impl(),
merely passing boost::variant<T0,T1,...> as the queue's value type rather
than the common T!
Naturally this could be extended to use wait_first_success()
semantics instead.
// No need to break out the first Fn for interface function: let the compiler// complain if empty.// Our functions have different return types, and we might have to return any// of them. Use a variant, expanding std::result_of<Fn()>::type for each Fn in// parameter pack.template<typename...Fns>boost::variant<typenamestd::result_of<Fns()>::type...>wait_first_value_het(Fns&&...functions){// Use buffered_channel<boost::variant<T1, T2, ...>>; see remarks above.typedefboost::variant<typenamestd::result_of<Fns()>::type...>return_t;typedefboost::fibers::buffered_channel<return_t>channel_t;autochanp(std::make_shared<channel_t>(64));// launch all the relevant fiberswait_first_value_impl<return_t>(chanp,std::forward<Fns>(functions)...);// retrieve the first valuereturn_tvalue(chanp->value_pop());// close the channel: no subsequent push() has to succeedchanp->close();returnvalue;}
It might be called like this:
boost::variant<std::string,double,int>result=wait_first_value_het([](){returnsleeper("wfvh_third",150);},[](){returnsleeper(3.14,100);},[](){returnsleeper(17,50);});std::cout<<"wait_first_value_het() => "<<result<<std::endl;assert(boost::get<int>(result)==17);when_any,
a dubious alternative
Certain topics in C++ can arouse strong passions, and exceptions are no
exception. We cannot resist mentioning — for purely informational purposes
— that when you need only the first result from some
number of concurrently-running fibers, it would be possible to pass a
shared_ptr<promise<>> to the
participating fibers, then cause the initiating fiber to call future::get() on
its future<>. The first fiber to call promise::set_value() on
that shared promise will
succeed; subsequent set_value() calls on the same promise
instance will throw future_error.
Use this information at your own discretion. Beware the dark side.
when_all functionalitywhen_all,
simple completion
For the case in which we must wait for all task functions
to complete — but we don't need results (or expect exceptions) from any of
them — we can write wait_all_simple() that looks remarkably like wait_first_simple().
The difference is that instead of our Done class, we instantiate a barrier and
call its barrier::wait().
We initialize the barrier
with (count+1)
because we are launching count
fibers, plus the wait()
call within wait_all_simple() itself.
template<typename...Fns>voidwait_all_simple(Fns&&...functions){std::size_tcount(sizeof...(functions));// Initialize a barrier(count+1) because we'll immediately wait on it. We// don't want to wake up until 'count' more fibers wait on it. Even though// we'll stick around until the last of them completes, use shared_ptr// anyway because it's easier to be confident about lifespan issues.autobarrier(std::make_shared<boost::fibers::barrier>(count+1));wait_all_simple_impl(barrier,std::forward<Fns>(functions)...);barrier->wait();}
As stated above, the only difference between wait_all_simple_impl() and wait_first_simple_impl()
is that the former calls barrier::wait() rather than Done::notify():
template<typenameFn,typename...Fns>voidwait_all_simple_impl(std::shared_ptr<boost::fibers::barrier>barrier,Fn&&function,Fns&&...functions){boost::fibers::fiber(std::bind([](std::shared_ptr<boost::fibers::barrier>&barrier,typenamestd::decay<Fn>::type&function)mutable{function();barrier->wait();},barrier,std::forward<Fn>(function))).detach();wait_all_simple_impl(barrier,std::forward<Fns>(functions)...);}
You might call it like this:
wait_all_simple([](){sleeper("was_long",150);},[](){sleeper("was_medium",100);},[](){sleeper("was_short",50);});
Control will not return from the wait_all_simple() call until the last of its task functions
has completed.
when_all,
return values
As soon as we want to collect return values from all the task functions,
we can see right away how to reuse wait_first_value()'s
queue<T> for the purpose. All we have to do is avoid closing it after
the first value!
But in fact, collecting multiple values raises an interesting question:
do we really want to wait until the slowest of them
has arrived? Wouldn't we rather process each result as soon as it becomes
available?
Fortunately we can present both APIs. Let's define wait_all_values_source() to return shared_ptr<buffered_channel<T>>.
Given wait_all_values_source(), it's straightforward to implement wait_all_values():
template<typenameFn,typename...Fns>std::vector<typenamestd::result_of<Fn()>::type>wait_all_values(Fn&&function,Fns&&...functions){std::size_tcount(1+sizeof...(functions));typedeftypenamestd::result_of<Fn()>::typereturn_t;typedefstd::vector<return_t>vector_t;vector_tresults;results.reserve(count);// get channelstd::shared_ptr<boost::fibers::buffered_channel<return_t>>chan=wait_all_values_source(std::forward<Fn>(function),std::forward<Fns>(functions)...);// fill results vectorreturn_tvalue;while(boost::fibers::channel_op_status::success==chan->pop(value)){results.push_back(value);}// return vector to callerreturnresults;}
It might be called like this:
std::vector<std::string>values=wait_all_values([](){returnsleeper("wav_late",150);},[](){returnsleeper("wav_middle",100);},[](){returnsleeper("wav_early",50);});
As you can see from the loop in wait_all_values(), instead of requiring its caller to count
values, we define wait_all_values_source() to buffered_channel::close() the
queue when done. But how do we do that? Each producer fiber is independent.
It has no idea whether it is the last one to buffered_channel::push() a
value.
We can address that problem with a counting façade
for the queue<>.
In fact, our façade need only support the producer end of the queue.
[wait_nqueue]
Armed with nqueue<>, we can implement wait_all_values_source().
It starts just like wait_first_value(). The difference is that we wrap
the queue<T>
with an nqueue<T>
to pass to the producer fibers.
Then, of course, instead of popping the first value, closing the queue
and returning it, we simply return the shared_ptr<queue<T>>.
// Return a shared_ptr<buffered_channel<T>> from which the caller can// retrieve each new result as it arrives, until 'closed'.template<typenameFn,typename...Fns>std::shared_ptr<boost::fibers::buffered_channel<typenamestd::result_of<Fn()>::type>>wait_all_values_source(Fn&&function,Fns&&...functions){std::size_tcount(1+sizeof...(functions));typedeftypenamestd::result_of<Fn()>::typereturn_t;typedefboost::fibers::buffered_channel<return_t>channel_t;// make the channelautochanp(std::make_shared<channel_t>(64));// and make an nchannel facade to close it after 'count' itemsautoncp(std::make_shared<nchannel<return_t>>(chanp,count));// pass that nchannel facade to all the relevant fiberswait_all_values_impl<return_t>(ncp,std::forward<Fn>(function),std::forward<Fns>(functions)...);// then return the channel for consumerreturnchanp;}
For example:
std::shared_ptr<boost::fibers::buffered_channel<std::string>>chan=wait_all_values_source([](){returnsleeper("wavs_third",150);},[](){returnsleeper("wavs_second",100);},[](){returnsleeper("wavs_first",50);});std::stringvalue;while(boost::fibers::channel_op_status::success==chan->pop(value)){std::cout<<"wait_all_values_source() => '"<<value<<"'"<<std::endl;}wait_all_values_impl() really is just like wait_first_value_impl()
except for the use of nqueue<T> rather than queue<T>:
template<typenameT,typenameFn>voidwait_all_values_impl(std::shared_ptr<nchannel<T>>chan,Fn&&function){boost::fibers::fiber([chan,function](){chan->push(function());}).detach();}when_all
until first exception
Naturally, just as with wait_first_outcome(),
we can elaborate wait_all_values() and wait_all_values_source()
by passing future<T>
instead of plain T.
wait_all_until_error() pops that future<T> and calls its future::get():
template<typenameFn,typename...Fns>std::vector<typenamestd::result_of<Fn()>::type>wait_all_until_error(Fn&&function,Fns&&...functions){std::size_tcount(1+sizeof...(functions));typedeftypenamestd::result_of<Fn()>::typereturn_t;typedeftypenameboost::fibers::future<return_t>future_t;typedefstd::vector<return_t>vector_t;vector_tresults;results.reserve(count);// get channelstd::shared_ptr<boost::fibers::buffered_channel<future_t>>chan(wait_all_until_error_source(std::forward<Fn>(function),std::forward<Fns>(functions)...));// fill results vectorfuture_tfuture;while(boost::fibers::channel_op_status::success==chan->pop(future)){results.push_back(future.get());}// return vector to callerreturnresults;}
For example:
std::stringthrown;try{std::vector<std::string>values=wait_all_until_error([](){returnsleeper("waue_late",150);},[](){returnsleeper("waue_middle",100,true);},[](){returnsleeper("waue_early",50);});}catch(std::exceptionconst&e){thrown=e.what();}std::cout<<"wait_all_until_error(fail) threw '"<<thrown<<"'"<<std::endl;Naturally this complicates the
API for wait_all_until_error_source(). The caller must both retrieve a future<T>
and call its get()
method. It would, of course, be possible to return a façade over the consumer
end of the queue that would implicitly perform the get() and return a simple T
(or throw).
The implementation is just as you would expect. Notice, however, that we
can reuse wait_first_outcome_impl(), passing the nqueue<T> rather than queue<T>.
// Return a shared_ptr<buffered_channel<future<T>>> from which the caller can// get() each new result as it arrives, until 'closed'.template<typenameFn,typename...Fns>std::shared_ptr<boost::fibers::buffered_channel<boost::fibers::future<typenamestd::result_of<Fn()>::type>>>wait_all_until_error_source(Fn&&function,Fns&&...functions){std::size_tcount(1+sizeof...(functions));typedeftypenamestd::result_of<Fn()>::typereturn_t;typedefboost::fibers::future<return_t>future_t;typedefboost::fibers::buffered_channel<future_t>channel_t;// make the channelautochanp(std::make_shared<channel_t>(64));// and make an nchannel facade to close it after 'count' itemsautoncp(std::make_shared<nchannel<future_t>>(chanp,count));// pass that nchannel facade to all the relevant fiberswait_first_outcome_impl<return_t>(ncp,std::forward<Fn>(function),std::forward<Fns>(functions)...);// then return the channel for consumerreturnchanp;}
For example:
typedefboost::fibers::future<std::string>future_t;std::shared_ptr<boost::fibers::buffered_channel<future_t>>chan=wait_all_until_error_source([](){returnsleeper("wauess_third",150);},[](){returnsleeper("wauess_second",100);},[](){returnsleeper("wauess_first",50);});future_tfuture;while(boost::fibers::channel_op_status::success==chan->pop(future)){std::stringvalue(future.get());std::cout<<"wait_all_until_error_source(success) => '"<<value<<"'"<<std::endl;}wait_all,
collecting all exceptionsGiven wait_all_until_error_source(),
it might be more reasonable to make a wait_all_...() that collects all
errors instead of presenting only the first:
template<typenameFn,typename...Fns>std::vector<typenamestd::result_of<Fn()>::type>wait_all_collect_errors(Fn&&function,Fns&&...functions){std::size_tcount(1+sizeof...(functions));typedeftypenamestd::result_of<Fn()>::typereturn_t;typedeftypenameboost::fibers::future<return_t>future_t;typedefstd::vector<return_t>vector_t;vector_tresults;results.reserve(count);exception_listexceptions("wait_all_collect_errors() exceptions");// get channelstd::shared_ptr<boost::fibers::buffered_channel<future_t>>chan(wait_all_until_error_source(std::forward<Fn>(function),std::forward<Fns>(functions)...));// fill results and/or exceptions vectorsfuture_tfuture;while(boost::fibers::channel_op_status::success==chan->pop(future)){std::exception_ptrexp=future.get_exception_ptr();if(!exp){results.push_back(future.get());}else{exceptions.add(exp);}}// if there were any exceptions, throwif(exceptions.size()){throwexceptions;}// no exceptions: return vector to callerreturnresults;}
The implementation is a simple variation on wait_first_success(),
using the same exception_list
exception class.
when_all,
heterogeneous types
But what about the case when we must wait for all results of different
types?
We can present an API that is frankly quite cool. Consider a sample struct:
structData{std::stringstr;doubleinexact;intexact;friendstd::ostream&operator<<(std::ostream&out,Dataconst&data);...};
Let's fill its members from task functions all running concurrently:
Datadata=wait_all_members<Data>([](){returnsleeper("wams_left",100);},[](){returnsleeper(3.14,150);},[](){returnsleeper(17,50);});std::cout<<"wait_all_members<Data>(success) => "<<data<<std::endl;
Note that for this case, we abandon the notion of capturing the earliest
result first, and so on: we must fill exactly the passed struct in left-to-right
order.
That permits a beautifully simple implementation:
// Explicitly pass Result. This can be any type capable of being initialized// from the results of the passed functions, such as a struct.template<typenameResult,typename...Fns>Resultwait_all_members(Fns&&...functions){// Run each of the passed functions on a separate fiber, passing all their// futures to helper function for processing.returnwait_all_members_get<Result>(boost::fibers::async(std::forward<Fns>(functions))...);}template<typenameResult,typename...Futures>Resultwait_all_members_get(Futures&&...futures){// Fetch the results from the passed futures into Result's initializer// list. It's true that the get() calls here will block the implicit// iteration over futures -- but that doesn't matter because we won't be// done until the slowest of them finishes anyway. As results are// processed in argument-list order rather than order of completion, the// leftmost get() to throw an exception will cause that exception to// propagate to the caller.returnResult{futures.get()...};}
It is tempting to try to implement wait_all_members() as a one-liner like this:
returnResult{boost::fibers::async(functions).get()...};
The trouble with this tactic is that it would serialize all the task functions.
The runtime makes a single pass through functions,
calling fibers::async() for each and then immediately calling
future::get() on its returned future<>. That blocks the implicit loop.
The above is almost equivalent to writing:
returnResult{functions()...};
in which, of course, there is no concurrency at all.
Passing the argument pack through a function-call boundary (wait_all_members_get())
forces the runtime to make two passes: one in wait_all_members()
to collect the future<>s
from all the async()
calls, the second in wait_all_members_get() to fetch each of the results.
As noted in comments, within the wait_all_members_get() parameter pack expansion pass, the blocking
behavior of get()
becomes irrelevant. Along the way, we will hit the get() for the slowest task function; after
that every subsequent get() will complete in trivial time.
By the way, we could also use this same API to fill a vector or other collection:
// If we don't care about obtaining results as soon as they arrive, and we// prefer a result vector in passed argument order rather than completion// order, wait_all_members() is another possible implementation of// wait_all_until_error().autostrings=wait_all_members<std::vector<std::string>>([](){returnsleeper("wamv_left",150);},[](){returnsleeper("wamv_middle",100);},[](){returnsleeper("wamv_right",50);});std::cout<<"wait_all_members<vector>() =>";for(std::stringconst&str:strings){std::cout<<" '"<<str<<"'";}std::cout<<std::endl;Sharing a
Thread with Another Main LoopOverview
As always with cooperative concurrency, it is important not to let any one
fiber monopolize the processor too long: that could starve
other ready fibers. This section discusses a couple of solutions.
Event-Driven
Program
Consider a classic event-driven program, organized around a main loop that
fetches and dispatches incoming I/O events. You are introducing Boost.Fiber because certain asynchronous I/O sequences
are logically sequential, and for those you want to write and maintain code
that looks and acts sequential.
You are launching fibers on the application’s main thread because certain
of their actions will affect its user interface, and the application’s UI
framework permits UI operations only on the main thread. Or perhaps those
fibers need access to main-thread data, and it would be too expensive in
runtime (or development time) to robustly defend every such data item with
thread synchronization primitives.
You must ensure that the application’s main loop itself
doesn’t monopolize the processor: that the fibers it launches will get the
CPU cycles they need.
The solution is the same as for any fiber that might claim the CPU for an
extended time: introduce calls to this_fiber::yield(). The
most straightforward approach is to call yield() on every iteration of your existing main
loop. In effect, this unifies the application’s main loop with Boost.Fiber’s
internal main loop. yield() allows the fiber manager to run any fibers
that have become ready since the previous iteration of the application’s main
loop. When these fibers have had a turn, control passes to the thread’s main
fiber, which returns from yield() and resumes the application’s main loop.
Embedded
Main Loop
More challenging is when the application’s main loop is embedded in some other
library or framework. Such an application will typically, after performing
all necessary setup, pass control to some form of run() function from which control does not return
until application shutdown.
A Boost.Asio
program might call io_service::run()
in this way.
In general, the trick is to arrange to pass control to this_fiber::yield() frequently.
You could use an Asio
timer for that purpose. You could instantiate the timer, arranging
to call a handler function when the timer expires. The handler function could
call yield(),
then reset the timer and arrange to wake up again on its next expiration.
Since, in this thought experiment, we always pass control to the fiber manager
via yield(),
the calling fiber is never blocked. Therefore there is always at least one
ready fiber. Therefore the fiber manager never calls algorithm::suspend_until().
Using io_service::post()
instead of setting a timer for some nonzero interval would be unfriendly
to other threads. When all I/O is pending and all fibers are blocked, the
io_service and the fiber manager would simply spin the CPU, passing control
back and forth to each other. Using a timer allows tuning the responsiveness
of this thread relative to others.
Deeper
Dive into Boost.Asio
By now the alert reader is thinking: but surely, with Asio in particular,
we ought to be able to do much better than periodic polling pings!
This turns out to be surprisingly tricky. We present a possible approach
in examples/asio/round_robin.hpp.
One consequence of using Boost.Asio
is that you must always let Asio suspend the running thread. Since Asio is
aware of pending I/O requests, it can arrange to suspend the thread in such
a way that the OS will wake it on I/O completion. No one else has sufficient
knowledge.
So the fiber scheduler must depend on Asio for suspension and resumption.
It requires Asio handler calls to wake it.
One dismaying implication is that we cannot support multiple threads calling
io_service::run()
on the same io_service instance.
The reason is that Asio provides no way to constrain a particular handler
to be called only on a specified thread. A fiber scheduler instance is locked
to a particular thread: that instance cannot manage any other thread’s fibers.
Yet if we allow multiple threads to call io_service::run()
on the same io_service instance,
a fiber scheduler which needs to sleep can have no guarantee that it will
reawaken in a timely manner. It can set an Asio timer, as described above
— but that timer’s handler may well execute on a different thread!
Another implication is that since an Asio-aware fiber scheduler (not to mention
boost::fibers::asio::yield)
depends on handler calls from the io_service,
it is the application’s responsibility to ensure that io_service::stop()
is not called until every fiber has terminated.
It is easier to reason about the behavior of the presented asio::round_robin scheduler if we require that
after initial setup, the thread’s main fiber is the fiber that calls io_service::run(),
so let’s impose that requirement.
Naturally, the first thing we must do on each thread using a custom fiber
scheduler is call use_scheduling_algorithm(). However,
since asio::round_robin requires an io_service
instance, we must first declare that.
std::shared_ptr<boost::asio::io_service>io_svc=std::make_shared<boost::asio::io_service>();boost::fibers::use_scheduling_algorithm<boost::fibers::asio::round_robin>(io_svc);use_scheduling_algorithm() instantiates asio::round_robin,
which naturally calls its constructor:
round_robin(std::shared_ptr<boost::asio::io_service>const&io_svc):io_svc_(io_svc),suspend_timer_(*io_svc_){// We use add_service() very deliberately. This will throw// service_already_exists if you pass the same io_service instance to// more than one round_robin instance.boost::asio::add_service(*io_svc_,newservice(*io_svc_));io_svc_->post([this]()mutable{asio::round_robin binds the passed io_service pointer and initializes a boost::asio::steady_timer:
std::shared_ptr<boost::asio::io_service>io_svc_;boost::asio::steady_timersuspend_timer_;
Then it calls boost::asio::add_service()
with a nested service struct:
structservice:publicboost::asio::io_service::service{staticboost::asio::io_service::idid;std::unique_ptr<boost::asio::io_service::work>work_;service(boost::asio::io_service&io_svc):boost::asio::io_service::service(io_svc),work_{newboost::asio::io_service::work(io_svc)}{}virtual~service(){}service(serviceconst&)=delete;service&operator=(serviceconst&)=delete;voidshutdown_service()overridefinal{work_.reset();}};
... [asio_rr_service_bottom]
The service struct has a
couple of roles.
Its foremost role is to manage a std::unique_ptr<boost::asio::io_service::work>. We want the
io_service instance to continue
its main loop even when there is no pending Asio I/O.
But when boost::asio::io_service::service::shutdown_service()
is called, we discard the io_service::work
instance so the io_service
can shut down properly.
Its other purpose is to post()
a lambda (not yet shown). Let’s walk further through the example program before
coming back to explain that lambda.
The service constructor returns
to asio::round_robin’s constructor, which returns
to use_scheduling_algorithm(), which returns to the application code.
Once it has called use_scheduling_algorithm(), the application may now launch some number
of fibers:
// servertcp::acceptora(*io_svc,tcp::endpoint(tcp::v4(),9999));boost::fibers::fiber(server,io_svc,std::ref(a)).detach();// clientconstunsignediterations=2;constunsignedclients=3;boost::fibers::barrierb(clients);for(unsignedi=0;i<clients;++i){boost::fibers::fiber(client,io_svc,std::ref(a),std::ref(b),iterations).detach();}
Since we don’t specify a launch, these fibers are ready to run,
but have not yet been entered.
Having set everything up, the application calls io_service::run():
io_svc->run();
Now what?
Because this io_service instance
owns an io_service::work instance, run() does not immediately return. But — none of
the fibers that will perform actual work has even been entered yet!
Without that initial post() call in service’s
constructor, nothing would happen. The application would
hang right here.
So, what should the post() handler execute? Simply this_fiber::yield()?
That would be a promising start. But we have no guarantee that any of the
other fibers will initiate any Asio operations to keep the ball rolling.
For all we know, every other fiber could reach a similar boost::this_fiber::yield() call first. Control would return to the
post()
handler, which would return to Asio, and... the application would hang.
The post()
handler could post()
itself again. But as discussed in the
previous section, once there are actual I/O operations in flight — once
we reach a state in which no fiber is ready —
that would cause the thread to
spin.
We could, of course, set an Asio timer — again as previously
discussed. But in this deeper dive, we’re trying to
do a little better.
The key to doing better is that since we’re in a fiber, we can run an actual
loop — not just a chain of callbacks. We can wait for something to happen
by calling io_service::run_one()
— or we can execute already-queued Asio handlers by calling io_service::poll().
Here’s the body of the lambda passed to the post() call.
while(!io_svc_->stopped()){if(has_ready_fibers()){// run all pending handlers in round_robinwhile(io_svc_->poll());// block this fiber till all pending (ready) fibers are processed// == round_robin::suspend_until() has been calledstd::unique_lock<boost::fibers::mutex>lk(mtx_);cnd_.wait(lk);}else{// run one handler inside io_service// if no handler available, block this threadif(!io_svc_->run_one()){break;}}}
We want this loop to exit once the io_service
instance has been stopped().
As long as there are ready fibers, we interleave running ready Asio handlers
with running ready fibers.
If there are no ready fibers, we wait by calling run_one(). Once any Asio handler has been called
— no matter which — run_one()
returns. That handler may have transitioned some fiber to ready state, so
we loop back to check again.
(We won’t describe awakened(), pick_next() or has_ready_fibers(), as these are just like round_robin::awakened(),
round_robin::pick_next() and round_robin::has_ready_fibers().)
That leaves suspend_until() and notify().
Doubtless you have been asking yourself: why are we calling io_service::run_one()
in the lambda loop? Why not call it in suspend_until(), whose very API was designed for just such
a purpose?
Under normal circumstances, when the fiber manager finds no ready fibers,
it calls algorithm::suspend_until(). Why test has_ready_fibers()
in the lambda loop? Why not leverage the normal mechanism?
The answer is: it matters who’s asking.
Consider the lambda loop shown above. The only Boost.Fiber
APIs it engages are has_ready_fibers() and this_fiber::yield().
yield()
does not block the calling fiber: the calling fiber
does not become unready. It is immediately passed back to algorithm::awakened(),
to be resumed in its turn when all other ready fibers have had a chance to
run. In other words: during a yield() call, there is always at least
one ready fiber.
As long as this lambda loop is still running, the fiber manager does not
call suspend_until()
because it always has a fiber ready to run.
However, the lambda loop itself can detect the case
when no other fibers are ready to run: the running fiber
is not ready but running.
That said, suspend_until() and notify() are in fact called during orderly shutdown
processing, so let’s try a plausible implementation.
voidsuspend_until(std::chrono::steady_clock::time_pointconst&abs_time)noexcept{// Set a timer so at least one handler will eventually fire, causing// run_one() to eventually return.if((std::chrono::steady_clock::time_point::max)()!=abs_time){// Each expires_at(time_point) call cancels any previous pending// call. We could inadvertently spin like this:// dispatcher calls suspend_until() with earliest wake time// suspend_until() sets suspend_timer_// lambda loop calls run_one()// some other asio handler runs before timer expires// run_one() returns to lambda loop// lambda loop yields to dispatcher// dispatcher finds no ready fibers// dispatcher calls suspend_until() with SAME wake time// suspend_until() sets suspend_timer_ to same time, canceling// previous async_wait()// lambda loop calls run_one()// asio calls suspend_timer_ handler with operation_aborted// run_one() returns to lambda loop... etc. etc.// So only actually set the timer when we're passed a DIFFERENT// abs_time value.suspend_timer_.expires_at(abs_time);suspend_timer_.async_wait([](boost::system::error_codeconst&){this_fiber::yield();});}cnd_.notify_one();}
As you might expect, suspend_until() sets an asio::steady_timer to expires_at()
the passed std::chrono::steady_clock::time_point.
Usually.
As indicated in comments, we avoid setting suspend_timer_
multiple times to the sametime_point
value since every expires_at() call cancels any previous async_wait()
call. There is a chance that we could spin. Reaching suspend_until() means the fiber manager intends to yield
the processor to Asio. Cancelling the previous async_wait() call would fire its handler, causing run_one()
to return, potentially causing the fiber manager to call suspend_until() again with the same time_point
value...
Given that we suspend the thread by calling io_service::run_one(), what’s important is that our async_wait()
call will cause a handler to run, which will cause run_one() to return. It’s not so important specifically
what that handler does.
voidnotify()noexcept{// Something has happened that should wake one or more fibers BEFORE// suspend_timer_ expires. Reset the timer to cause it to fire// immediately, causing the run_one() call to return. In theory we// could use cancel() because we don't care whether suspend_timer_'s// handler is called with operation_aborted or success. However --// cancel() doesn't change the expiration time, and we use// suspend_timer_'s expiration time to decide whether it's already// set. If suspend_until() set some specific wake time, then notify()// canceled it, then suspend_until() was called again with the same// wake time, it would match suspend_timer_'s expiration time and we'd// refrain from setting the timer. So instead of simply calling// cancel(), reset the timer, which cancels the pending sleep AND sets// a new expiration time. This will cause us to spin the loop twice --// once for the operation_aborted handler, once for timer expiration// -- but that shouldn't be a big problem.suspend_timer_.async_wait([](boost::system::error_codeconst&){this_fiber::yield();});suspend_timer_.expires_at(std::chrono::steady_clock::now());}
Since an expires_at()
call cancels any previous async_wait() call, we can make notify() simply call steady_timer::expires_at(). That should cause the io_service
to call the async_wait()
handler with operation_aborted.
The comments in notify()
explain why we call expires_at() rather than cancel().
This boost::fibers::asio::round_robin implementation is used in
examples/asio/autoecho.cpp.
It seems possible that you could put together a more elegant Fiber / Asio
integration. But as noted at the outset: it’s tricky.
Specualtive
executionHardware
transactional memory
With help of hardware transactional memory multiple logical processors execute
a critical region speculatively, e.g. without explicit synchronization.
If the transactional execution completes successfully, then all memory operations
performed within the transactional region are commited without any inter-thread
serialization. When the optimistic execution fails, the processor aborts
the transaction and discards all performed modifications. In non-transactional
code a single lock serializes the access to a critical region. With a transactional
memory, multiple logical processor start a transaction and update the memory
(the data) inside the ciritical region. Unless some logical processors try
to update the same data, the transactions would always succeed.
Intel
Transactional Synchronisation Extensions (TSX)
TSX is Intel's implementation of hardware transactional memory in modern Intel
processors
intel.com: Intel
Transactional Synchronization Extensions. In TSX the hardware keeps track of which cachelines have
been read from and which have been written to in a transaction. The cache-line
size (64-byte) and the n-way set associative cache determine the maximum size
of memory in a transaction. For instance if a transaction modifies 9 cache-lines
at a processor with a 8-way set associative cache, the transaction will always
be aborted.
TXS is enabled if property htm=tsx is
specified at b2 command-line and BOOST_USE_TSX
is applied to the compiler.
A TSX-transaction will be aborted if the floating point state is modified
inside a critical region. As a consequence floating point operations, e.g.
store/load of floating point related registers during a fiber (context) switch
are disabled.
TSX can not be used together with MSVC at this time!
Boost.Fiber uses TSX-enabled spinlocks to protect critical regions (see section
Tuning).
NUMA
Modern micro-processors contain integrated memory controllers that are connected
via channels to the memory. Accessing the memory can be organized in two kinds:
Uniform Memory Access (UMA) and Non-Uniform Memory Access (NUMA).
In contrast to UMA, that provides a centralized pool of memory (and thus does
not scale after a certain number of processors), a NUMA architecture divides
the memory into local and remote memory relative to the micro-processor.
Local memory is directly attached to the processor's integrated memory controller.
Memory connected to the memory controller of another micro-processor (multi-socket
systems) is considered as remote memory. If a memory controller access remote
memory it has to traverse the interconnect
On x86 the interconnection is implemented by Intel's Quick Path Interconnect
(QPI) and AMD's HyperTransport.
and connect to the remote memory controller. Thus accessing
remote memory adds additional latency overhead to local memory access. Because
of the different memory locations, a NUMA-system experiences non-uniform
memory access time. As a consequence the best performance is achieved
by keeping the memory access local.
NUMANUMA
support in Boost.Fiber
Because only a subset of the NUMA-functionality is exposed by several operating
systems, Boost.Fiber provides only a minimalistic NUMA API.
In order to enable NUMA support, b2 property numa=on must
be specified and linked against additional library libboost_fiber_numa.so.
MinGW using pthread implementation is not supported on Windows.
Supported functionality/operating systems
AIX
FreeBSD
HP/UX
Linux
Solaris
Windows
pin thread
+
+
+
+
+
+
logical CPUs/NUMA nodes
+
+
+
+
+
+
Windows organizes logical cpus in groups of 64; boost.fiber maps
{group-id,cpud-id} to a scalar equivalent to cpu ID of Linux (64
* group ID + cpu ID).
NUMA node distance
-
-
-
+
-
-
tested on
AIX 7.2
FreeBSD 11
-
Arch Linux (4.10.13)
OpenIndiana HIPSTER
Windows 10
In order to keep the memory access local as possible, the NUMA topology must
be evaluated.
std::vector<boost::fibers::numa::node>topo=boost::fibers::numa::topology();for(auton:topo){std::cout<<"node: "<<n.id<<" | ";std::cout<<"cpus: ";for(autocpu_id:n.logical_cpus){std::cout<<cpu_id<<" ";}std::cout<<"| distance: ";for(autod:n.distance){std::cout<<d<<" ";}std::cout<<std::endl;}std::cout<<"done"<<std::endl;output:node:0|cpus:012345671617181920212223|distance:1021node:1|cpus:891011121314152425262728293031|distance:2110done
The example shows that the systems consits out of 2 NUMA-nodes, to each NUMA-node
belong 16 logical cpus. The distance measures the costs to access the memory
of another NUMA-node. A NUMA-node has always a distance 10
to itself (lowest possible value). The position in the array corresponds
with the NUMA-node ID.
Some work-loads benefit from pinning threads to a logical cpus. For instance
scheduling algorithm numa::work_stealing pins the thread
that runs the fiber scheduler to a logical cpu. This prevents the operating
system scheduler to move the thread to another logical cpu that might run other
fiber scheduler(s) or migrating the thread to a logical cpu part of another
NUMA-node.
voidthread(std::uint32_tcpu_id,std::uint32_tnode_id,std::vector<boost::fibers::numa::node>const&topo){// thread registers itself at work-stealing schedulerboost::fibers::use_scheduling_algorithm<boost::fibers::algo::numa::work_stealing>(cpu_id,node_id,topo);...}// evaluate the NUMA topologystd::vector<boost::fibers::numa::node>topo=boost::fibers::numa::topology();// start-thread runs on NUMA-node `0`autonode=topo[0];// start-thread is pinnded to first cpu ID in the list of logical cpus of NUMA-node `0`autostart_cpu_id=*node.logical_cpus.begin();// start worker-threads firststd::vector<std::thread>threads;for(auto&node:topo){for(std::uint32_tcpu_id:node.logical_cpus){// exclude start-threadif(start_cpu_id!=cpu_id){// spawn threadthreads.emplace_back(thread,cpu_id,node.id,std::cref(topo));}}}// start-thread registers itself on work-stealing schedulerboost::fibers::use_scheduling_algorithm<boost::fibers::algo::numa::work_stealing>(start_cpu_id,node.id,topo);...
The example evaluates the NUMA topology with boost::fibers::numa::topology()
and spawns for each logical cpu a thread. Each spawned thread installs the
NUMA-aware work-stealing scheduler. The scheduler pins the thread to the logical
cpu that was specified at construction. If the local queue of one thread
runs out of ready fibers, the thread tries to steal a ready fiber from another
thread running at logical cpu that belong to the same NUMA-node (local memory
access). If no fiber could be stolen, the thread tries to steal fibers from
logical cpus part of other NUMA-nodes (remote memory access).
Synopsis
#include<boost/fiber/numa/pin_thread.hpp>#include<boost/fiber/numa/topology.hpp>namespaceboost{namespacefibers{namespacenuma{structnode{std::uint32_tid;std::set<std::uint32_t>logical_cpus;std::vector<std::uint32_t>distance;};booloperator<(nodeconst&,nodeconst&)noexcept;std::vector<node>topology();voidpin_thread(std::uint32_t);voidpin_thread(std::uint32_t,std::thread::native_handle_type);}}}#include<boost/fiber/numa/algo/work_stealing.hpp>namespaceboost{namespacefibers{namespacenuma{namespacealgo{classwork_stealing;}}}
Class numa::node#include<boost/fiber/numa/topology.hpp>namespaceboost{namespacefibers{namespacenuma{structnode{std::uint32_tid;std::set<std::uint32_t>logical_cpus;std::vector<std::uint32_t>distance;};booloperator<(nodeconst&,nodeconst&)noexcept;}}}
Data member idstd::uint32_tid;Effects:
ID of the NUMA-node
Data
member logical_cpusstd::set<std::uint32_t>logical_cpus;Effects:
set of logical cpu IDs belonging to the NUMA-node
Data member
distancestd::vector<std::uint32_t>distance;Effects:
The distance between NUMA-nodes describe the cots of accessing the remote
memory.
Note:
A NUMA-node has a distance of 10
to itself, remote NUMA-nodes have a distance > 10.
The index in the array corresponds to the ID id
of the NUMA-node. At the moment only Linux returns the correct distances,
for all other operating systems remote NUMA-nodes get a default value
of 20.
Member
function operator<()
booloperator<(nodeconst&lhs,nodeconst&rhs)constnoexcept;Returns:true if lhs!=rhs
is true and the implementation-defined total order of node::id
values places lhs before
rhs, false otherwise.
Throws:
Nothing.
Non-member function numa::topology()#include<boost/fiber/numa/topology.hpp>namespaceboost{namespacefibers{namespacenuma{std::vector<node>topology();}}}Effects:
Evaluates the NUMA topology.
Returns:
a vector of NUMA-nodes describing the NUMA architecture of the system
(each element represents a NUMA-node).
Throws:system_error
Non-member function
numa::pin_thread()#include<boost/fiber/numa/pin_thread.hpp>namespaceboost{namespacefibers{namespacenuma{voidpin_thread(std::uint32_tcpu_id);voidpin_thread(std::uint32_tcpu_id,std::thread::native_handle_typeh);}}}Effects:
First version pins thisthread to the logical cpu with ID
cpu_id, e.g. the operating
system scheduler will not migrate the thread to another logical cpu.
The second variant pins the thread with the native ID h
to logical cpu with ID cpu_id.
Throws:system_error
Class
numa::work_stealing
This class implements algorithm; the thread running this scheduler
is pinned to the given logical cpu. If the local ready-queue runs out of ready
fibers, ready fibers are stolen from other schedulers that run on logical cpus
that belong to the same NUMA-node (local memory access). If no ready
fibers can be stolen from the local NUMA-node, the algorithm selects schedulers
running on other NUMA-nodes (remote memory access). The victim scheduler
(from which a ready fiber is stolen) is selected at random.
#include<boost/fiber/numa/algo/work_stealing.hpp>namespaceboost{namespacefibers{namespacenuma{namespacealgo{classwork_stealing:publicalgorithm{public:work_stealing(std::uint32_tcpu_id,std::uint32_tnode_id,std::vector<boost::fibers::numa::node>const&topo,boolsuspend=false);work_stealing(work_stealingconst&)=delete;work_stealing(work_stealing&&)=delete;work_stealing&operator=(work_stealingconst&)=delete;work_stealing&operator=(work_stealing&&)=delete;virtualvoidawakened(context*)noexcept;virtualcontext*pick_next()noexcept;virtualboolhas_ready_fibers()constnoexcept;virtualvoidsuspend_until(std::chrono::steady_clock::time_pointconst&)noexcept;virtualvoidnotify()noexcept;};}}}}Constructor
work_stealing(std::uint32_tcpu_id,std::uint32_tnode_id,std::vector<boost::fibers::numa::node>const&topo,boolsuspend=false);Effects:
Constructs work-stealing scheduling algorithm. The thread is pinned to
logical cpu with ID cpu_id.
If local ready-queue runs out of ready fibers, ready fibers are stolen
from other schedulers using topology
(represents the NUMA-topology of the system).
Throws:system_errorNote:
If suspend is set to
true, then the scheduler
suspends if no ready fiber could be stolen. The scheduler will by woken
up if a sleeping fiber times out or it was notified from remote (other
thread or fiber scheduler).
Member
function awakened()
virtualvoidawakened(context*f)noexcept;Effects:
Enqueues fiber f onto
the shared ready queue.
Throws:
Nothing.
Member
function pick_next()
virtualcontext*pick_next()noexcept;Returns:
the fiber at the head of the ready queue, or nullptr
if the queue is empty.
Throws:
Nothing.
Note:
Placing ready fibers onto the tail of the sahred queue, and returning
them from the head of that queue, shares the thread between ready fibers
in round-robin fashion.
Member
function has_ready_fibers()
virtualboolhas_ready_fibers()constnoexcept;Returns:true if scheduler has fibers
ready to run.
Throws:
Nothing.
Member
function suspend_until()
virtualvoidsuspend_until(std::chrono::steady_clock::time_pointconst&abs_time)noexcept;Effects:
Informs work_stealing
that no ready fiber will be available until time-point abs_time. This implementation blocks
in std::condition_variable::wait_until().
Throws:
Nothing.
Member
function notify()
virtualvoidnotify()noexcept=0;Effects:
Wake up a pending call to work_stealing::suspend_until(),
some fibers might be ready. This implementation wakes suspend_until() via std::condition_variable::notify_all().
Throws:
Nothing.
GPU computingCUDACUDA (Compute Unified
Device Architecture) is a platform for parallel computing on NVIDIA
GPUs. The application programming interface of CUDA gives access to GPU's
instruction set and computation resources (Execution of compute kernels).
Synchronization
with CUDA streams
CUDA operation such as compute kernels or memory transfer (between host and
device) can be grouped/queued by CUDA streams. are executed on the GPUs.
Boost.Fiber enables a fiber to sleep (suspend) till a CUDA stream has completed
its operations. This enables applications to run other fibers on the CPU
without the need to spawn an additional OS-threads. And resume the fiber
when the CUDA streams has finished.
__global__voidkernel(intsize,int*a,int*b,int*c){intidx=threadIdx.x+blockIdx.x*blockDim.x;if(idx<size){intidx1=(idx+1)%256;intidx2=(idx+2)%256;floatas=(a[idx]+a[idx1]+a[idx2])/3.0f;floatbs=(b[idx]+b[idx1]+b[idx2])/3.0f;c[idx]=(as+bs)/2;}}boost::fibers::fiberf([&done]{cudaStream_tstream;cudaStreamCreate(&stream);intsize=1024*1024;intfull_size=20*size;int*host_a,*host_b,*host_c;cudaHostAlloc(&host_a,full_size*sizeof(int),cudaHostAllocDefault);cudaHostAlloc(&host_b,full_size*sizeof(int),cudaHostAllocDefault);cudaHostAlloc(&host_c,full_size*sizeof(int),cudaHostAllocDefault);int*dev_a,*dev_b,*dev_c;cudaMalloc(&dev_a,size*sizeof(int));cudaMalloc(&dev_b,size*sizeof(int));cudaMalloc(&dev_c,size*sizeof(int));std::minstd_randgenerator;std::uniform_int_distribution<>distribution(1,6);for(inti=0;i<full_size;++i){host_a[i]=distribution(generator);host_b[i]=distribution(generator);}for(inti=0;i<full_size;i+=size){cudaMemcpyAsync(dev_a,host_a+i,size*sizeof(int),cudaMemcpyHostToDevice,stream);cudaMemcpyAsync(dev_b,host_b+i,size*sizeof(int),cudaMemcpyHostToDevice,stream);kernel<<<size/256,256,0,stream>>>(size,dev_a,dev_b,dev_c);cudaMemcpyAsync(host_c+i,dev_c,size*sizeof(int),cudaMemcpyDeviceToHost,stream);}autoresult=boost::fibers::cuda::waitfor_all(stream);// suspend fiber till CUDA stream has finishedBOOST_ASSERT(stream==std::get<0>(result));BOOST_ASSERT(cudaSuccess==std::get<1>(result));std::cout<<"f1: GPU computation finished"<<std::endl;cudaFreeHost(host_a);cudaFreeHost(host_b);cudaFreeHost(host_c);cudaFree(dev_a);cudaFree(dev_b);cudaFree(dev_c);cudaStreamDestroy(stream);});f.join();Synopsis
#include<boost/fiber/cuda/waitfor.hpp>namespaceboost{namespacefibers{namespacecuda{std::tuple<cudaStream_t,cudaError_t>waitfor_all(cudaStream_tst);std::vector<std::tuple<cudaStream_t,cudaError_t>>waitfor_all(cudaStream_t...st);}}}
Non-member function cuda::waitfor()#include<boost/fiber/cuda/waitfor.hpp>namespaceboost{namespacefibers{namespacecuda{std::tuple<cudaStream_t,cudaError_t>waitfor_all(cudaStream_tst);std::vector<std::tuple<cudaStream_t,cudaError_t>>waitfor_all(cudaStream_t...st);}}}Effects:
Suspends active fiber till CUDA stream has finished its operations.
Returns:
tuple of stream reference and the CUDA stream status
ROCm/HIPHIP
is part of the ROC (Radeon Open Compute)
platform for parallel computing on AMD and NVIDIA GPUs. The application programming
interface of HIP gives access to GPU's instruction set and computation resources
(Execution of compute kernels).
Synchronization
with ROCm/HIP streams
HIP operation such as compute kernels or memory transfer (between host and
device) can be grouped/queued by HIP streams. are executed on the GPUs. Boost.Fiber
enables a fiber to sleep (suspend) till a HIP stream has completed its operations.
This enables applications to run other fibers on the CPU without the need
to spawn an additional OS-threads. And resume the fiber when the HIP streams
has finished.
__global__voidkernel(intsize,int*a,int*b,int*c){intidx=threadIdx.x+blockIdx.x*blockDim.x;if(idx<size){intidx1=(idx+1)%256;intidx2=(idx+2)%256;floatas=(a[idx]+a[idx1]+a[idx2])/3.0f;floatbs=(b[idx]+b[idx1]+b[idx2])/3.0f;c[idx]=(as+bs)/2;}}boost::fibers::fiberf([&done]{hipStream_tstream;hipStreamCreate(&stream);intsize=1024*1024;intfull_size=20*size;int*host_a,*host_b,*host_c;hipHostMalloc(&host_a,full_size*sizeof(int),hipHostMallocDefault);hipHostMalloc(&host_b,full_size*sizeof(int),hipHostMallocDefault);hipHostMalloc(&host_c,full_size*sizeof(int),hipHostMallocDefault);int*dev_a,*dev_b,*dev_c;hipMalloc(&dev_a,size*sizeof(int));hipMalloc(&dev_b,size*sizeof(int));hipMalloc(&dev_c,size*sizeof(int));std::minstd_randgenerator;std::uniform_int_distribution<>distribution(1,6);for(inti=0;i<full_size;++i){host_a[i]=distribution(generator);host_b[i]=distribution(generator);}for(inti=0;i<full_size;i+=size){hipMemcpyAsync(dev_a,host_a+i,size*sizeof(int),hipMemcpyHostToDevice,stream);hipMemcpyAsync(dev_b,host_b+i,size*sizeof(int),hipMemcpyHostToDevice,stream);hipLaunchKernel(kernel,dim3(size/256),dim3(256),0,stream,size,dev_a,dev_b,dev_c);hipMemcpyAsync(host_c+i,dev_c,size*sizeof(int),hipMemcpyDeviceToHost,stream);}autoresult=boost::fibers::hip::waitfor_all(stream);// suspend fiber till HIP stream has finishedBOOST_ASSERT(stream==std::get<0>(result));BOOST_ASSERT(hipSuccess==std::get<1>(result));std::cout<<"f1: GPU computation finished"<<std::endl;hipHostFree(host_a);hipHostFree(host_b);hipHostFree(host_c);hipFree(dev_a);hipFree(dev_b);hipFree(dev_c);hipStreamDestroy(stream);});f.join();Synopsis
#include<boost/fiber/hip/waitfor.hpp>namespaceboost{namespacefibers{namespacehip{std::tuple<hipStream_t,hipError_t>waitfor_all(hipStream_tst);std::vector<std::tuple<hipStream_t,hipError_t>>waitfor_all(hipStream_t...st);}}}
Non-member function hip::waitfor()#include<boost/fiber/hip/waitfor.hpp>namespaceboost{namespacefibers{namespacehip{std::tuple<hipStream_t,hipError_t>waitfor_all(hipStream_tst);std::vector<std::tuple<hipStream_t,hipError_t>>waitfor_all(hipStream_t...st);}}}Effects:
Suspends active fiber till HIP stream has finished its operations.
Returns:
tuple of stream reference and the HIP stream status
Running with worker
threadsKeep
workers running
If a worker thread is used but no fiber is created or parts of the framework
(like this_fiber::yield()) are touched, then no fiber scheduler
is instantiated.
autoworker=std::thread([]{// fiber scheduler not instantiated});worker.join();
If use_scheduling_algorithm<>() is invoked, the
fiber scheduler is created. If the worker thread simply returns, destroys the
scheduler and terminates.
autoworker=std::thread([]{// fiber scheduler createdboost::fibers::use_scheduling_algorithm<my_fiber_scheduler>();});worker.join();
In order to keep the worker thread running, the fiber associated with the thread
stack (which is called main fiber) is blocked. For instance
the main fiber might wait on a condition_variable.
For a gracefully shutdown condition_variable is signalled
and the main fiber returns. The scheduler gets destructed if
all fibers of the worker thread have been terminated.
boost::fibers::mutexmtx;boost::fibers::condition_variable_anycv;autoworker=std::thread([&mtx,&cv]{mtx.lock();// suspend till signalledcv.wait(mtx);mtx.unlock();});// signal terminationcv.notify_all();worker.join();Processing
tasks
Tasks can be transferred via channels. The worker thread runs a pool of fibers
that dequeue and executed tasks from the channel. The termination is signalled
via closing the channel.
usingtask=std::function<void()>;boost::fibers::buffered_channel<task>ch{1024};autoworker=std::thread([&ch]{// create pool of fibersfor(inti=0;i<10;++i){boost::fibers::fiber{[&ch]{tasktsk;// dequeue and process taskswhile(boost::fibers::channel_op_status::closed!=ch.pop(tsk)){tsk();}}}.detach();}tasktsk;// dequeue and process taskswhile(boost::fibers::channel_op_status::closed!=ch.pop(tsk)){tsk();}});// feed channel with tasksch.push([]{...});...// signal terminationch.close();worker.join();
An alternative is to use a work-stealing scheduler. This kind of scheduling
algorithm a worker thread steals fibers from the ready-queue of other worker
threads if its own ready-queue is empty.
Wait till all worker threads have registered the work-stealing scheduling
algorithm.
boost::fibers::mutexmtx;boost::fibers::condition_variable_anycv;// start wotrker-thread firstautoworker=std::thread([&mtx,&cv]{boost::fibers::use_scheduling_algorithm<boost::fibers::algo::work_stealing>(2);mtx.lock();// suspend main-fiber from the worker threadcv.wait(mtx);mtx.unlock();});boost::fibers::use_scheduling_algorithm<boost::fibers::algo::work_stealing>(2);// create fibers with tasksboost::fibers::fiberf{[]{...}};...// signal terminationcv.notify_all();worker.join();
Because the TIB (thread information block on Windows) is not fully described
in the MSDN, it might be possible that not all required TIB-parts are swapped.
Using WinFiber implementation might be an alternative (see documentation
about implementations
fcontext_t, ucontext_t and WinFiber of boost.context).
Performance
Performance measurements were taken using std::chrono::highresolution_clock,
with overhead corrections. The code was compiled with gcc-6.3.1, using build
options: variant = release, optimization = speed. Tests were executed on dual
Intel XEON E5 2620v4 2.2GHz, 16C/32T, 64GB RAM, running Linux (x86_64).
Measurements headed 1C/1T were run in a single-threaded process.
The microbenchmark syknet
from Alexander Temerev was ported and used for performance measurements. At
the root the test spawns 10 threads-of-execution (ToE), e.g. actor/goroutine/fiber
etc.. Each spawned ToE spawns additional 10 ToEs ... until 1,000,000
ToEs are created. ToEs return back their ordinal numbers (0 ... 999,999), which
are summed on the previous level and sent back upstream, until reaching the
root. The test was run 10-20 times, producing a range of values for each measurement.
time per actor/erlang process/goroutine (other languages) (average over
1,000,000)
Haskell | stack-1.4.0/ghc-8.0.1
Go | go1.8.1
Erlang | erts-8.3
0.05 µs - 0.06 µs
0.42 µs - 0.49 µs
0.63 µs - 0.73 µs
Pthreads are created with a stack size of 8kB while std::thread's
use the system default (1MB - 2MB). The microbenchmark could not
be run with 1,000,000 threads because of
resource exhaustion (pthread and std::thread).
Instead the test runs only at 10,000 threads.
time per thread (average over 10,000 - unable to spawn 1,000,000 threads)
pthread
std::threadstd::async
54 µs - 73 µs
52 µs - 73 µs
106 µs - 122 µs
The test utilizes 16 cores with Symmetric MultiThreading enabled (32 logical
CPUs). The fiber stacks are allocated by fixedsize_stack.
As the benchmark shows, the memory allocation algorithm is significant for
performance in a multithreaded environment. The tests use glibc’s memory allocation
algorithm (based on ptmalloc2) as well as Google’s TCmalloc
(via linkflags="-ltcmalloc").
Tais B. Ferreira, Rivalino Matias, Autran Macedo, Lucio B. Araujo An
Experimental Study on Memory Allocators in Multicore and Multithreaded Applications,
PDCAT ’11 Proceedings of the 2011 12th International Conference on Parallel
and Distributed Computing, Applications and Technologies, pages 92-98
In the work_stealing scheduling algorithm, each thread has
its own local queue. Fibers that are ready to run are pushed to and popped
from the local queue. If the queue runs out of ready fibers, fibers are stolen
from the local queues of other participating threads.
time per fiber (average over 1.000.000)
fiber (16C/32T, work stealing, tcmalloc)
fiber (1C/1T, round robin, tcmalloc)
0.05 µs - 0.09 µs
1.69 µs - 1.79 µs
TuningDisable
synchronization
With BOOST_FIBERS_NO_ATOMICS
defined at the compiler’s command line, synchronization between fibers (in different
threads) is disabled. This is acceptable if the application is single threaded
and/or fibers are not synchronized between threads.
Memory
allocation
Memory allocation algorithm is significant for performance in a multithreaded
environment, especially for Boost.Fiber where
fiber stacks are allocated on the heap. The default user-level memory allocator
(UMA) of glibc is ptmalloc2 but it can be replaced by another UMA that fit
better for the concret work-load For instance Google’s TCmalloc
enables a better performance at the skynet microbenchmark
than glibc’s default memory allocator.
Scheduling
strategies
The fibers in a thread are coordinated by a fiber manager. Fibers trade control
cooperatively, rather than preemptively. Depending on the work-load several
strategies of scheduling the fibers are possible
1024cores.net: Task
Scheduling Strategies that can be implmented on behalf of algorithm.
work-stealing: ready fibers are hold in a local queue, when the fiber-scheduler's
local queue runs out of ready fibers, it randomly selects another fiber-scheduler
and tries to steal a ready fiber from the victim (implemented in work_stealing and
numa::work_stealing)
work-requesting: ready fibers are hold in a local queue, when the fiber-scheduler's
local queue runs out of ready fibers, it randomly selects another fiber-scheduler
and requests for a ready fibers, the victim fiber-scheduler sends a ready-fiber
back
work-sharing: ready fibers are hold in a global queue, fiber-scheduler
concurrently push and pop ready fibers to/from the global queue (implemented
in shared_work)
work-distribution: fibers that became ready are proactivly distributed
to idle fiber-schedulers or fiber-schedulers with low load
work-balancing: a dedicated (helper) fiber-scheduler periodically collects
informations about all fiber-scheduler running in other threads and re-distributes
ready fibers among them
TTAS
locks
Boost.Fiber uses internally spinlocks to protect critical regions if fibers
running on different threads interact. Spinlocks are implemented as TTAS (test-test-and-set)
locks, i.e. the spinlock tests the lock before calling an atomic exchange.
This strategy helps to reduce the cache line invalidations triggered by acquiring/releasing
the lock.
Spin-wait
loop
A lock is considered under contention, if a thread repeatedly fails to acquire
the lock because some other thread was faster. Waiting for a short time lets
other threads finish before trying to enter the critical section again. While
busy waiting on the lock, relaxing the CPU (via pause/yield mnemonic) gives
the CPU a hint that the code is in a spin-wait loop.
prevents expensive pipeline flushes (speculatively executed load and compare
instructions are not pushed to pipeline)
another hardware thread (simultaneous multithreading) can get time slice
it does delay a few CPU cycles, but this is necessary to prevent starvation
It is obvious that this strategy is useless on single core systems because
the lock can only released if the thread gives up its time slice in order to
let other threads run. The macro BOOST_FIBERS_SPIN_SINGLE_CORE replaces the
CPU hints (pause/yield mnemonic) by informing the operating system (via std::this_thread_yield()) that the thread gives up its time slice
and the operating system switches to another thread.
Exponential
back-off
The macro BOOST_FIBERS_RETRY_THRESHOLD determines how many times the CPU iterates
in the spin-wait loop before yielding the thread or blocking in futex-wait.
The spinlock tracks how many times the thread failed to acquire the lock. The
higher the contention, the longer the thread should back-off. A Binary
Exponential Backoff algorithm together with a randomized contention
window is utilized for this purpose. BOOST_FIBERS_CONTENTION_WINDOW_THRESHOLD
determines the upper limit of the contention window (expressed as the exponent
for basis of two).
Speculative
execution (hardware transactional memory)
Boost.Fiber uses spinlocks to protect critical regions that can be used together
with transactional memory (see section Speculative
execution).
TXS is enabled if property htm=tsx is
specified at b2 command-line and BOOST_USE_TSX
is applied to the compiler.
A TSX-transaction will be aborted if the floating point state is modified
inside a critical region. As a consequence floating point operations, e.g.
tore/load of floating point related registers during a fiber (context) switch
are disabled.
NUMA
systems
Modern multi-socket systems are usually designed as NUMA
systems. A suitable fiber scheduler like numa::work_stealing reduces
remote memory access (latence).
Parameters
Parameters that migh be defiend at compiler's command line
Parameter
Default value
Effect on Boost.Fiber
BOOST_FIBERS_NO_ATOMICS
-
no multithreading support, all atomics removed, no synchronization
between fibers running in different threads
BOOST_FIBERS_SPINLOCK_STD_MUTEX
-
std::mutex used inside spinlock
BOOST_FIBERS_SPINLOCK_TTAS
+
spinlock with test-test-and-swap on shared variable
BOOST_FIBERS_SPINLOCK_TTAS_ADAPTIVE
-
spinlock with test-test-and-swap on shared variable, adaptive retries
while busy waiting
BOOST_FIBERS_SPINLOCK_TTAS_FUTEX
-
spinlock with test-test-and-swap on shared variable, suspend on futex
after certain number of retries
BOOST_FIBERS_SPINLOCK_TTAS_ADAPTIVE_FUTEX
-
spinlock with test-test-and-swap on shared variable, while busy waiting
adaptive retries, suspend on futex certain amount of retries
BOOST_FIBERS_SPINLOCK_TTAS + BOOST_USE_TSX
-
spinlock with test-test-and-swap and speculative execution (Intel
TSX required)
BOOST_FIBERS_SPINLOCK_TTAS_ADAPTIVE + BOOST_USE_TSX
-
spinlock with test-test-and-swap on shared variable, adaptive retries
while busy waiting and speculative execution (Intel TSX required)
BOOST_FIBERS_SPINLOCK_TTAS_FUTEX + BOOST_USE_TSX
-
spinlock with test-test-and-swap on shared variable, suspend on futex
after certain number of retries and speculative execution (Intel
TSX required)
BOOST_FIBERS_SPINLOCK_TTAS_ADAPTIVE_FUTEX + BOOST_USE_TSX
-
spinlock with test-test-and-swap on shared variable, while busy waiting
adaptive retries, suspend on futex certain amount of retries and
speculative execution (Intel TSX required)
BOOST_FIBERS_SPIN_SINGLE_CORE
-
on single core machines with multiple threads, yield thread (std::this_thread::yield())
after collisions
BOOST_FIBERS_RETRY_THRESHOLD
64
max number of retries while busy spinning, the use fallback
BOOST_FIBERS_CONTENTION_WINDOW_THRESHOLD
16
max size of collisions window, expressed as exponent for the basis
of two
BOOST_FIBERS_SPIN_BEFORE_SLEEP0
32
max number of retries that relax the processor before the thread
sleeps for 0s
BOOST_FIBERS_SPIN_BEFORE_YIELD
64
max number of retries where the thread sleeps for 0s before yield
thread (std::this_thread::yield())
CustomizationOverview
As noted in the Scheduling section, by default
Boost.Fiber uses its own round_robin scheduler
for each thread. To control the way Boost.Fiber
schedules ready fibers on a particular thread, in general you must follow several
steps. This section discusses those steps, whereas Scheduling
serves as a reference for the classes involved.
The library's fiber manager keeps track of suspended (blocked) fibers. Only
when a fiber becomes ready to run is it passed to the scheduler. Of course,
if there are fewer than two ready fibers, the scheduler's job is trivial. Only
when there are two or more ready fibers does the particular scheduler implementation
start to influence the overall sequence of fiber execution.
In this section we illustrate a simple custom scheduler that honors an integer
fiber priority. We will implement it such that a fiber with higher priority
is preferred over a fiber with lower priority. Any fibers with equal priority
values are serviced on a round-robin basis.
The full source code for the examples below is found in priority.cpp.
Custom
Property Class
The first essential point is that we must associate an integer priority with
each fiber.
A previous version of the Fiber library implicitly tracked an int priority
for each fiber, even though the default scheduler ignored it. This has been
dropped, since the library now supports arbitrary scheduler-specific fiber
properties.
One might suggest deriving a custom fiber subclass to store such
properties. There are a couple of reasons for the present mechanism.
Boost.Fiber provides a number of different
ways to launch a fiber. (Consider fibers::async().) Higher-level
libraries might introduce additional such wrapper functions. A custom scheduler
must associate its custom properties with every fiber
in the thread, not only the ones explicitly launched by instantiating a
custom fiber subclass.
Consider a large existing program that launches fibers in many different
places in the code. We discover a need to introduce a custom scheduler
for a particular thread. If supporting that scheduler's custom properties
required a particular fiber
subclass, we would have to hunt down and modify every place that launches
a fiber on that thread.
The fiber class is actually just a handle to internal context data.
A subclass of fiber would
not add data to context.
The present mechanism allows you to drop in a custom scheduler
with its attendant custom properties without altering
the rest of your application.
Instead of deriving a custom scheduler fiber properties subclass from fiber,
you must instead derive it from fiber_properties.
classpriority_props:publicboost::fibers::fiber_properties{public:priority_props(boost::fibers::context*ctx):fiber_properties(ctx),priority_(0){}intget_priority()const{returnpriority_;}// Call this method to alter priority, because we must notify// priority_scheduler of any change.voidset_priority(intp){// Of course, it's only worth reshuffling the queue and all if we're// actually changing the priority.if(p!=priority_){priority_=p;notify();}}// The fiber name of course is solely for purposes of this example// program; it has nothing to do with implementing scheduler priority.// This is a public data member -- not requiring set/get access methods --// because we need not inform the scheduler of any change.std::stringname;private:intpriority_;};
Your subclass constructor must accept a context*
and pass it to the fiber_properties
constructor.
Provide read access methods at your own discretion.
It's important to call notify() on any change in a property that can
affect the scheduler's behavior. Therefore, such modifications should only
be performed through an access method.
A property that does not affect the scheduler does not need access methods.
Custom
Scheduler Class
Now we can derive a custom scheduler from algorithm_with_properties<>,
specifying our custom property class priority_props
as the template parameter.
classpriority_scheduler:publicboost::fibers::algo::algorithm_with_properties<priority_props>{private:typedefboost::fibers::scheduler::ready_queue_typerqueue_t;rqueue_trqueue_;std::mutexmtx_{};std::condition_variablecnd_{};boolflag_{false};public:priority_scheduler():rqueue_(){}// For a subclass of algorithm_with_properties<>, it's important to// override the correct awakened() overload.virtualvoidawakened(boost::fibers::context*ctx,priority_props&props)noexcept{intctx_priority=props.get_priority();// With this scheduler, fibers with higher priority values are// preferred over fibers with lower priority values. But fibers with// equal priority values are processed in round-robin fashion. So when// we're handed a new context*, put it at the end of the fibers// with that same priority. In other words: search for the first fiber// in the queue with LOWER priority, and insert before that one.rqueue_t::iteratori(std::find_if(rqueue_.begin(),rqueue_.end(),[ctx_priority,this](boost::fibers::context&c){returnproperties(&c).get_priority()<ctx_priority;}));// Now, whether or not we found a fiber with lower priority,// insert this new fiber here.rqueue_.insert(i,*ctx);}virtualboost::fibers::context*pick_next()noexcept{// if ready queue is empty, just tell callerif(rqueue_.empty()){returnnullptr;}boost::fibers::context*ctx(&rqueue_.front());rqueue_.pop_front();returnctx;}virtualboolhas_ready_fibers()constnoexcept{return!rqueue_.empty();}virtualvoidproperty_change(boost::fibers::context*ctx,priority_props&props)noexcept{// Although our priority_props class defines multiple properties, only// one of them (priority) actually calls notify() when changed. The// point of a property_change() override is to reshuffle the ready// queue according to the updated priority value.// 'ctx' might not be in our queue at all, if caller is changing the// priority of (say) the running fiber. If it's not there, no need to// move it: we'll handle it next time it hits awakened().if(!ctx->ready_is_linked()){return;}// Found ctx: unlink itctx->ready_unlink();// Here we know that ctx was in our ready queue, but we've unlinked// it. We happen to have a method that will (re-)add a context* to the// right place in the ready queue.awakened(ctx,props);}voidsuspend_until(std::chrono::steady_clock::time_pointconst&time_point)noexcept{if((std::chrono::steady_clock::time_point::max)()==time_point){std::unique_lock<std::mutex>lk(mtx_);cnd_.wait(lk,[this](){returnflag_;});flag_=false;}else{std::unique_lock<std::mutex>lk(mtx_);cnd_.wait_until(lk,time_point,[this](){returnflag_;});flag_=false;}}voidnotify()noexcept{std::unique_lock<std::mutex>lk(mtx_);flag_=true;lk.unlock();cnd_.notify_all();}};
See ready_queue_t.
You must override the algorithm_with_properties::awakened()
method.
This is how your scheduler receives notification of a fiber that has become
ready to run.
props is the instance of
priority_props associated with the passed fiber ctx.
You must override the algorithm_with_properties::pick_next()
method.
This is how your scheduler actually advises the fiber manager of the next
fiber to run.
You must override algorithm_with_properties::has_ready_fibers()
to
inform the fiber manager of the state of your ready queue.
Overriding algorithm_with_properties::property_change()
is
optional. This override handles the case in which the running fiber changes
the priority of another ready fiber: a fiber already in our queue. In that
case, move the updated fiber within the queue.
Your property_change()
override must be able to handle the case in which the passed ctx is not in your ready queue. It might
be running, or it might be blocked.
Our example priority_scheduler
doesn't override algorithm_with_properties::new_properties():
we're content with allocating priority_props
instances on the heap.
Replace
Default Scheduler
You must call use_scheduling_algorithm() at the start
of each thread on which you want Boost.Fiber
to use your custom scheduler rather than its own default round_robin.
Specifically, you must call use_scheduling_algorithm() before performing any other Boost.Fiber
operations on that thread.
intmain(intargc,char*argv[]){// make sure we use our priority_scheduler rather than default round_robinboost::fibers::use_scheduling_algorithm<priority_scheduler>();...}Use
Properties
The running fiber can access its own fiber_properties subclass
instance by calling this_fiber::properties(). Although
properties<>()
is a nullary function, you must pass, as a template parameter, the fiber_properties subclass.
boost::this_fiber::properties<priority_props>().name="main";
Given a fiber instance still connected with a running fiber (that
is, not fiber::detach()ed), you may access that fiber's properties
using fiber::properties(). As with boost::this_fiber::properties<>(), you must pass your fiber_properties subclass as the template
parameter.
template<typenameFn>boost::fibers::fiberlaunch(Fn&&func,std::stringconst&name,intpriority){boost::fibers::fiberfiber(func);priority_props&props(fiber.properties<priority_props>());props.name=name;props.set_priority(priority);returnfiber;}
Launching a new fiber schedules that fiber as ready, but does not
immediately enter its fiber-function. The current fiber
retains control until it blocks (or yields, or terminates) for some other reason.
As shown in the launch()
function above, it is reasonable to launch a fiber and immediately set relevant
properties -- such as, for instance, its priority. Your custom scheduler can
then make use of this information next time the fiber manager calls algorithm_with_properties::pick_next().
Rationalepreprocessor
defines
preopcessor defines
BOOST_FIBERS_NO_ATOMICS
no std::atomic<>
used, inter-thread synchronization disabled
BOOST_FIBERS_SPINLOCK_STD_MUTEX
use std::mutex as spinlock instead of default
XCHG-sinlock with
backoff
BOOST_FIBERS_SPIN_BACKOFF
limit determines when to used std::this_thread::yield() instead of mnemonic pause/yield during busy wait (apllies
on to XCHG-spinlock)
BOOST_FIBERS_SINGLE_CORE
allways call std::this_thread::yield() without backoff during busy wait
(apllies on to XCHG-spinlock)
distinction
between coroutines and fibers
The fiber library extends the coroutine library by adding a scheduler and synchronization
mechanisms.
a coroutine yields
a fiber blocks
When a coroutine yields, it passes control directly to its caller (or, in the
case of symmetric coroutines, a designated other coroutine). When a fiber blocks,
it implicitly passes control to the fiber scheduler. Coroutines have no scheduler
because they need no scheduler.'N4024:
Distinguishing coroutines and fibers'.
what
about transactional memory
GCC supports transactional memory since version 4.7. Unfortunately tests show
that transactional memory is slower (ca. 4x) than spinlocks using atomics.
Once transactional memory is improved (supporting hybrid tm), spinlocks will
be replaced by __transaction_atomic{} statements surrounding the critical sections.
synchronization
between fibers running in different threads
Synchronization classes from Boost.Thread
block the entire thread. In contrast, the synchronization classes from Boost.Fiber block only specific fibers, so that the
scheduler can still keep the thread busy running other fibers in the meantime.
The synchronization classes from Boost.Fiber
are designed to be thread-safe, i.e. it is possible to synchronize fibers running
in different threads as well as fibers running in the same thread. (However,
there is a build option to disable cross-thread fiber synchronization support;
see this description.)
spurious
wakeup
Spurious wakeup can happen when using std::condition_variable:
the condition variable appears to be have been signaled while the awaited condition
may still be false. Spurious wakeup can happen repeatedly and is caused on
some multiprocessor systems where making std::condition_variable
wakeup completely predictable would slow down all std::condition_variable
operations.
David R. Butenhof Programming with POSIX Threadscondition_variable is not subject to spurious wakeup.
Nonetheless it is prudent to test the business-logic condition in a wait() loop
— or, equivalently, use one of the wait(lock,predicate)
overloads.
See also No Spurious Wakeups.
migrating
fibers between threads
Support for migrating fibers between threads has been integrated. The user-defined
scheduler must call context::detach() on a fiber-context on the
source thread and context::attach() on the destination thread,
passing the fiber-context to migrate. (For more information about custom schedulers,
see Customization.) Examples work_sharing
and work_stealing in directory
examples might be used as a
blueprint.
See also Migrating fibers between threads.
support
for Boost.Asio
Support for Boost.Asio’s
async-result is not part of the official API. However,
to integrate with a boost::asio::io_service,
see Sharing a Thread with Another Main Loop.
To interface smoothly with an arbitrary Asio async I/O operation, see Then There’s Boost.Asio.
tested
compilers
The library was tested with GCC-5.1.1, Clang-3.6.0 and MSVC-14.0 in c++11-mode.
supported
architectures
Boost.Fiber depends on Boost.Context
- the list of supported architectures can be found here.
Acknowledgments
I'd like to thank Agustín Bergé, Eugene Yakubovich, Giovanni Piero Deretta
and especially Nat Goodspeed.