SkillAgentSearch skills...

Fiber

C++20 fiber implementation with similar interface to std::thread, header-only / x86_64 / Linux only / stackful / built-in scheduler / thread shareable

Install / Use

/learn @fix8mt/Fiber

README

<p align="center"> <a href="https://www.fix8mt.com"><img src="https://github.com/fix8mt/fiber/blob/main/assets/fix8mt_Master_Logo_Green_Trans.png" width="200"></a> </p>

fiber

A novel C++20 fiber implementation with similar interface to std::thread, header-only / x86_64 / Linux only / stackful / built-in scheduler / thread shareable


Introduction

This is a novel fiber implementation with a similar interface to std::thread. Taking any callable object, fibers execute and cooperatively yield amongst themselves and the calling function all within a single thread. Using fibers, single threaded applications can be written as though they were multi-threaded, with the advantage that no concurrency controls are required. For multi-threaded applications, each thread maintains its own list of running fibers, leaving the user to implement their own concurrency controls. This implementation allows you to move fibers between threads. This can be used to share work or scale an application by adding more fibers to new or existing threads.

Currently only Linux/x86_64 is supported. Other platforms to be supported in the future.

| montest2 - example monitor application | |:--:| | Screenshot from montest2 with 20 fibers working in one thread and using fiber_monitor|

Quick links

|Link|Description| --|-- |Wiki| for complete documentation| |API| for API documentation| |Building| for build options and settings| |Monitor| for built-in monitor documentation| |Examples| for details about all the included examples| |Here| for implementation| |Here| for the original f8fiber implementation|

Motivation

  • header-only
  • std::thread like interface
  • no external dependencies
  • simplicity, lightweight
  • make use of C++20 features
  • constexpr where possible
  • expand and improve interface

Features

  • x86_64 Linux only
  • single header-only
  • stackful fibers; stacksize configurable for each fiber
  • supports any callable object (eg. function, class member, lambda expression) with optional arguments
  • heap based, memory-mapped or placement stacks; user definable stack can also be used
  • context switch implemented with inline assembly
  • fibers can be moved to other threads (can be configured out for performance)
  • std::invoke style ctor, with arguments perfectly forwarded to the callable
  • extended API, supporting resume, resume_if, resume_with, kill, kill_all, suspend, schedule, join, join_if, detach, resume_main, schedule_if, move, suspend_if, wait_all and more
  • built-in fiber printer
  • optional built-in terminal based monitor
  • helper templates including async, make_fiber, launch_all and launch_all_with_params and more
  • supports fibers and this_fiber namespaces
  • ctor based fiber parameter struct (POD) - including fiber name, custom stack and stack size, launch policy, launch order and auto join
  • built-in round-robin scheduler
  • can be used with std::packaged_task, std::future, std::promise and so forth
  • fast, very lightweight
  • fiber exceptions and general exception handling
  • built-in instrumentation (can be configured out for performance)
  • lots of examples
  • full API documentation
  • supports jfiber (similar to std::jthread)
  • works with gcc, clang

Examples

1. A simple resumable function

In the following example each iteration of main and func simply yields, printing the iteration count before and after each yield. Note to pass a reference to the flags variable we need to use the std::ref wrapper. Before exiting func calls the built-in printer. When func exits, control returns to main where testing f0 for non-zero returns false indicating the fiber has finished.

Note that you can name a fiber using fiber_params. The default name for the calling thread of a fiber is main.

<details><summary><i>source</i></summary> <p>
#include <iostream>
#include <iomanip>
#include <thread>
#include <string>
#include <fix8/fiber.hpp>
using namespace FIX8;

void func(bool& flags, int cnt)
{
   std::cout << '\t' << this_fiber::name() << " (fiber id:" << this_fiber::get_id() << ")\n";
   for (int kk{}; kk < cnt; ++kk)
   {
      std::cout << '\t' << this_fiber::name() << ':' << kk << '\n';
      this_fiber::yield();
      std::cout << '\t' << this_fiber::name() << ':' << kk << " (resumed)\n";
   }
   flags = true;
   fibers::print();
   std::cout << '\t' << this_fiber::name() << ":exit\n";
}

int main(int argc, char *argv[])
{
   std::cout << this_fiber::name() << ":entry (fiber id:" << this_fiber::get_id() << ")\n";
   bool flags{};
   fiber f0({"func"}, &func, std::ref(flags), 5);
   std::cout << "flags=" << std::boolalpha << flags << '\n';

   for (int ii{}; f0; ++ii)
   {
      std::cout << this_fiber::name() << ':' << ii << '\n';
      this_fiber::yield();
      std::cout << this_fiber::name() << ':' << ii << " (resumed)\n";
   }
   std::cout << "flags=" << std::boolalpha << flags << '\n';
   std::cout << this_fiber::name() << ":exit\n";
   return 0;
}
</p> </details> <details><summary><i>output</i></summary> <p>
$ ./fibertest0
main:entry (fiber id:NaF)
flags=false
main:0
        func:entry (fiber id:2896)
        func:0
main:0 (resumed)
main:1
        func:0 (resumed)
        func:1
main:1 (resumed)
main:2
        func:1 (resumed)
        func:2
main:2 (resumed)
main:3
        func:2 (resumed)
        func:3
main:3 (resumed)
main:4
        func:3 (resumed)
        func:4
main:4 (resumed)
main:5
        func:4 (resumed)
#      fid  pfid prev   ctxs      stack ptr    stack alloc    depth  stacksz     flags ord name
0    * 2896 NaF  NaF       6 0x7f9fb3632ea8 0x7f9fb3613010      352   131072 _________  99 func
1      NaF  NaF  2896      6 0x7fff7628f8a8              0        0  8388608 m________  99 main
        func:exit
main:5 (resumed)
flags=true
main:exit
$
</p> </details>

2. A generator class

The following example implements a simple generator pattern.

Both the producer() and consumer() methods of foo are passed as callable objects to the fiber members _produce and _consume.

Note that the fiber ctor requires this for pointer to member functions. Also note the additional parameter num passed to _produce. Your callable object definition must match the parameters passed to fiber.

After construction of the fibers, the foo ctor calls _produce.resume() which immediately switches to producer(). Each iteration of the for loop generates 5 random numbers that are pushed to a queue after which producer() switches to consumer().

Note that consumer() will only continue to run while the _produce fiber is still running, consuming all numbers pushed to the queue and when empty switching back to producer().

When producer() has looped numtogen times, it calls _consume.schedule() which instructs the fiber scheduler to schedule consumer() as the next fiber to run; producer() then exits and consumer() is resumed (exiting a fiber causes the scheduler to switch to the next scheduled fiber) - but since the _produce fiber has exited, while (_produce) will return false causing consumer() to also exit.

<details><summary><i>source</i></summary> <p>
#include <queue>
#include <random>
#include <fix8/fiber.hpp>
using namespace FIX8;

class foo
{
   std::queue<long> _queue;
   fiber _produce, _consume;

   void producer(int numtogen)
   {
      std::cout << "\tproducer:entry (id:" << this_fiber::get_id() << ")\n";
      std::mt19937_64 rnde {std::random_device{}()};
      auto dist{std::uniform_int_distribution<long>(1, std::numeric_limits<long>().max())};
      for (; numtogen; --numtogen)
      {
         while(_queue.size() < 5)
            _queue.push(dist(rnde));
         std::cout << "\tproduced: " << _queue.size() << '\n';
         _consume.resume(); // switch to consumer
      }
      _consume.schedule(); // consumer is next fiber to run
      std::cout << "\tproducer:exit\n";
   }
   void consumer()
   {
      std::cout << "\tconsumer:entry (id:" << this_fiber::get_id() << ")\n";
      while (_produce)
      {
         int cnt{};
         while(!_queue.empty())
         {
            std::cout << "\t\t" << ++cnt << ": " << _queue.front() << '\n';
            _queue.pop();
         }
         std::cout << "\tconsumed: " << cnt << '\n';
         _produce.resume(); // switch to producer
      }
      std::cout << "\tconsumer:exit\n";
   }

public:
   foo(int num) : _produce(&foo::producer, this, num), _consume(&foo::consumer, this)
   {
      _produce.resume(); // switch to producer
   }
};

int main(int argc, char *argv[])
{
   std::cout << "main:entry\n";
   foo bar(argc > 1 ? std::stoi(argv[1]) : 10);
   std::cout << "main:exit\n";
   return 0;
}
</p> </details> <details><summary><i>output</i></summary> <p>
$ ./fibertest22 5
main:entry
  producer:entry (id:4016)
  produced: 5
  consumer:entry (id:1968)
	 1: 1349602525532272804
	 2: 316583067409009491
	 3: 1020347932332564514
	 4: 3829997051190076899
	 5: 947478241006829049
  consumed: 5
  produced: 5
	 1: 3617976082375098752
	 2: 3972849389773859934
	 3: 7998668485491537141
	 4: 4650831140486621276
	 5: 7332043501
View on GitHub
GitHub Stars30
CategoryDevelopment
Updated4mo ago
Forks3

Languages

C++

Security Score

92/100

Audited on Nov 12, 2025

No findings