Boost C++ Libraries

...one of the most highly regarded and expertly designed C++ library projects in the world. Herb Sutter and Andrei Alexandrescu, C++ Coding Standards

This is the documentation for a snapshot of the develop branch, built from commit 549cd0dc90.
PrevUpHomeNext

Context switching with fibers

Implementations: fcontext_t, ucontext_t and WinFiber
Class fiber
[Note] 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

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] 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().

Usage

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.

Parameter passing

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.

Exception handling

If the function executed inside a context-function emits ans exception, the application is terminated by calling std::terminate(). std::exception_ptr can be used to transfer exceptions between different fibers.

[Important] Important

Do not jump from inside a catch block and then re-throw the exception in another fiber.

Executing function on top of a fiber

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 void.

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=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.

Stack unwinding

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] 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 stack

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] 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} {
    }
    ...
};

Inverting the control flow

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.


PrevUpHomeNext