...one of the most highly
regarded and expertly designed C++ library projects in the
world.
— Herb Sutter and Andrei
Alexandrescu, C++
Coding Standards
Note | |
---|---|
fiber is the reference implementation of C++ proposal P0876R0: fibers without scheduler. |
A fiber represents the state of the control flow of a program at a given point in time. Fibers can be suspended and resumed later in order to change the control flow of a program.
Modern micro-processors are registers machines; the content of processor registers represent a fiber of the executed program at a given point in time. Operating systems simulate parallel execution of programs on a single processor by switching between programs (context switch) by preserving and restoring the fiber, e.g. the content of all registers.
fiber captures the current fiber (the rest of the computation; code after fiber) and triggers a context switch. The context switch is achieved by preserving certain registers (including instruction and stack pointer), defined by the calling convention of the ABI, of the current fiber and restoring those registers of the resumed fiber. The control flow of the resumed fiber continues. The current fiber is suspended and passed as argument to the resumed fiber.
fiber expects a context-function
with signature 'fiber(fiber && f)'
.
The parameter f
represents
the current fiber from which this fiber was resumed (e.g. that has called
fiber).
On return the context-function of the current fiber has to specify an fiber to which the execution control is transferred after termination of the current fiber.
If an instance with valid state goes out of scope and the context-function has not yet returned, the stack is traversed in order to access the control structure (address stored at the first stack frame) and fiber's stack is deallocated via the StackAllocator.
Note | |
---|---|
Segmented stacks are supported by fiber using ucontext_t. |
fiber represents a fiber; it contains the content of preserved registers and manages the associated stack (allocation/deallocation). fiber is a one-shot fiber - it can be used only once, after calling continuation::resume() or continuation::resume_with() it is invalidated.
fiber is only move-constructible and move-assignable.
As a first-class object fiber can be applied to and returned from a function, assigned to a variable or stored in a container.
A fiber is continued by calling resume()
/resume_with()
.
namespace ctx=boost::context; int a; ctx::fiber source{[&a](ctx::fiber&& sink){ a=0; int b=1; for(;;){ sink=std::move(sink).resume(); int next=a+b; a=b; b=next; } return std::move(sink); }}; for (int j=0;j<10;++j) { source=std::move(source).resume(); std::cout << a << " "; } output: 0 1 1 2 3 5 8 13 21 34
This simple example demonstrates the basic usage of fiber
as a generator. The fiber sink
represents the main-fiber (function main()
). sink
is captured (current-fiber) by invoking fiber
and passed as parameter to the lambda.
Because the state is invalidated (one-shot fiber) by each call of continuation::resume(),
the new state of the fiber,
returned by continuation::resume(), needs to be assigned
to sink
after each call. In
order to express the invalidation of the resumed fiber, the member functions
resume()
and resume_with()
are rvalue-ref qualified. Both functions bind only to rvalues. Thus an lvalue
fiber must be casted to an rvalue via std::move()
.
The lambda that calculates the Fibonacci numbers is executed inside the fiber
represented by source
. Calculated
Fibonacci numbers are transferred between the two fibers via variable a
(lambda capture reference).
The locale variables b
and
next
remain their values during
each context switch. This is possible due source
has its own stack and the stack is exchanged by each context switch.
Data can be transferred between two fibers via global pointers, calling wrappers
(like std::bind
) or lambda captures.
namespace ctx=boost::context; int i=1; ctx::fiber f1{[&i](ctx::fiber&& f2){ std::printf("inside f1,i==%d\n",i); i+=1; return std::move(f2).resume(); }}; f1=std::move(f1).resume(); std::printf("i==%d\n",i); output: inside c1,i==1 i==2
f1.resume()
enters the lambda in fiber represented by f1
with lambda capture reference i=1
. The expression
f2.resume()
resumes the fiber f2
. On return
of f1.resume()
,
the variable i
has the value
of i+1
.
If the function executed inside a context-function emits
an exception, the application is terminated by calling std::terminate()
. std::exception_ptr
can be used to transfer exceptions between different fibers.
Important | |
---|---|
Do not jump from inside a catch block. |
Sometimes it is useful to execute a new function on top of a resumed fiber. For this purpose continuation::resume_with() has to be used. The function passed as argument must accept a rvalue reference to fiber and return fiber.
namespace ctx=boost::context; int data=0; ctx::fiber f1{[&data](ctx::fiber&& f2) { std::cout << "f1: entered first time: " << data << std::endl; data+=1; f2=std::move(f2).resume(); std::cout << "f1: entered second time: " << data << std::endl; data+=1; f2=std::move(f2).resume(); std::cout << "f1: entered third time: " << data << std::endl; return std::move(f2); }}; f1=std::move(f1).resume(); std::cout << "f1: returned first time: " << data << std::endl; data+=1; f1=std::move(f1).resume(); std::cout << "f1: returned second time: " << data << std::endl; data+=1; f1=std::move(f1).resume_with([&data](ctx::fiber&& f2){ std::cout << "f2: entered: " << data << std::endl; data=-1; return std::move(f2); }); std::cout << "f1: returned third time" << std::endl; output: f1: entered first time: 0 f1: returned first time: 1 f1: entered second time: 2 f1: returned second time: 3 f2: entered: 4 f1: entered third time: -1 f1: returned third time
The expression f1.resume_with(...)
executes a lambda on top of fiber f1
,
e.g. an additional stack frame is allocated on top of the stack. This lambda
assigns -1
to data
and returns to the
second invocation of f1.resume()
.
Another option is to execute a function on top of the fiber that throws an exception.
namespace ctx=boost::context; struct my_exception : public std::runtime_error { ctx::fiber f; my_exception(ctx::fiber&& f_,std::string const& what) : std::runtime_error{ what }, f{ std::move(f_) } { } }; ctx::fiber f{[](ctx::fiber && f) ->ctx::fiber { std::cout << "entered" << std::endl; try { f=std::move(f).resume(); } catch (my_exception & ex) { std::cerr << "my_exception: " << ex.what() << std::endl; return std::move(ex.f); } return {}; }); f=std::move(f).resume(); f=std::move(f).resume_with([](ctx::fiber && f) ->ctx::fiber { throw my_exception(std::move(f),"abc"); return {}; }); output: entered my_exception: abc
In this exception my_exception
is throw from a function invoked on-top of fiber f
and catched inside the for
-loop.
On construction of fiber a stack
is allocated. If the context-function returns the stack
will be destructed. If the context-function has not yet
returned and the destructor of an valid fiber
instance (e.g. fiber::operator bool() returns true
) is called, the stack will be destructed
too.
Important | |
---|---|
Code executed by context-function must not prevent the propagation ofs the detail::forced_unwind exception. Absorbing that exception will cause stack unwinding to fail. Thus, any code that catches all exceptions must re-throw any pending detail::forced_unwind exception. |
Allocating control structures on top of the stack requires to allocated the stack_context and create the control structure with placement new before fiber is created.
Note | |
---|---|
The user is responsible for destructing the control structure at the top of the stack. |
namespace ctx=boost::context; // stack-allocator used for (de-)allocating stack fixedsize_stack salloc(4048); // allocate stack space stack_context sctx(salloc.allocate()); // reserve space for control structure on top of the stack void * sp=static_cast<char*>(sctx.sp)-sizeof(my_control_structure); std::size_t size=sctx.size-sizeof(my_control_structure); // placement new creates control structure on reserved space my_control_structure * cs=new(sp)my_control_structure(sp,size,sctx,salloc); ... // destructing the control structure cs->~my_control_structure(); ... struct my_control_structure { // captured fiber ctx::fiber f; template< typename StackAllocator > my_control_structure(void * sp,std::size_t size,stack_context sctx,StackAllocator salloc) : // create captured fiber f{std::allocator_arg,preallocated(sp,size,sctx),salloc,entry_func} { } ... };
namespace ctx=boost::context; /* * grammar: * P ---> E '\0' * E ---> T {('+'|'-') T} * T ---> S {('*'|'/') S} * S ---> digit | '(' E ')' */ class Parser{ char next; std::istream& is; std::function<void(char)> cb; char pull(){ return std::char_traits<char>::to_char_type(is.get()); } void scan(){ do{ next=pull(); } while(isspace(next)); } public: Parser(std::istream& is_,std::function<void(char)> cb_) : next(), is(is_), cb(cb_) {} void run() { scan(); E(); } private: void E(){ T(); while (next=='+'||next=='-'){ cb(next); scan(); T(); } } void T(){ S(); while (next=='*'||next=='/'){ cb(next); scan(); S(); } } void S(){ if (isdigit(next)){ cb(next); scan(); } else if(next=='('){ cb(next); scan(); E(); if (next==')'){ cb(next); scan(); }else{ throw std::runtime_error("parsing failed"); } } else{ throw std::runtime_error("parsing failed"); } } }; std::istringstream is("1+1"); // user-code pulls parsed data from parser // invert control flow char c; bool done=false; // execute parser in new fiber ctx::fiber source{[&is,&c,&done](ctx::fiber&& sink){ // create parser with callback function Parser p(is, [&sink,&c](char c_){ // resume main fiber c=c_; sink=std::move(sink).resume(); }); // start recursive parsing p.run(); // signal termination done=true; // resume main fiber return std::move(sink); }}; source=std::move(source).resume(); while(!done){ printf("Parsed: %c\n",c); source=std::move(source).resume(); } output: Parsed: 1 Parsed: + Parsed: 1
In this example a recursive descent parser uses a callback to emit a newly passed symbol. Using fiber the control flow can be inverted, e.g. the user-code pulls parsed symbols from the parser - instead to get pushed from the parser (via callback).
The data (character) is transferred between the two fibers.