| Home | Libraries | People | FAQ | More

## Context switching with fibers

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