说到进程、线程,大部分接触过程序的人可能都听说过,倒是 fiber(纤程)更少为人知一点,而且每个地方谈到这个概念的时候又会有一些细微的差别,所以我们这里仅仅尝试罗列一些特性,然后来看看到底有些什么部分大家是一致的。
概念
先看看 wikipedia 上面的说法
In computer science, a fiber is a particularly lightweight thread of execution.
Like threads, fibers share address space. However, fibers use cooperative multitasking while threads use preemptive multitasking. Threads often depend on the kernel’s thread scheduler to preempt a busy thread and resume another thread; fibers yield themselves to run another fiber while executing.
wikipedia
也可以去看看比如 boost.fiber 的文档,
Boost.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….
Boost.Fiber
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….
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.
同一个作者也提交给标准委员会一个提议,这里面谈到
Because of name clashes with coroutine from coroutine TS, execution context from executor proposals and continuation used
in the context of future::then(), fiber, as suggested by some committee members, has been chosen – filament, tasklet,
lightfiber or coop_task are possible alternative names.The minimal API enables stackful context switching without the need for a scheduler. The API is suitable to act as building-
p0876r0
block for high-level constructs such as stackful coroutines as well as cooperative multitasking (aka user-land/green threads
that incorporate a scheduling facility).
另外 Meta / Facebook 也提供了一个实现 folly::fibers
Fibers (or coroutines) are lightweight application threads. Multiple fibers can be running on top of a single system thread. Unlike system threads, all the context switching between fibers is happening explicitly. Because of this every such context switch is very fast (~200 million of fiber context switches can be made per second on a single CPU core).
folly::fibers implements a task manager (FiberManager), which executes scheduled tasks on fibers. It also provides some fiber-compatible synchronization primitives.
folly::fibers
我们这里小结一下:
- fiber 是用户级别的 thread:线程的调度器一般是 OS 提供,同时是 preemptive(被动失去执行的 context);而纤程的调度不依赖 OS,同时是协作式(主动的提供位置允许其他纤程执行)
- fiber 也许可以不需要 scheduler,但是 boost 也好、folly 也好都是提供了 scheduler,这使得他们可以与其他的消息系统整合
- 在较低抽象层上,fiber 可以用来实现 coroutine(这更多的是从 programming construct 上定义的一种形式),也可以提供一套类似 thread 之间同步的工具帮助使用 fiber 的用户能更方便的写相关的程序
- fiber 提供 concurrency,但是并不提供 parallelism;thread 提供两者(如果只有一个 core 就会退变成 concurrency):stackoverflow 有个有意思的比方,一个人需要在一天里面做两件事情,拿护照、做演讲稿,所谓的 concurrency 指的是多件任务可以在重叠的时间里开始、执行、完成,因此这个人这两件事可以先去发证机关排上队(可能要好几个小时),然后开始准备演讲稿,等到被人叫号的时候处理护照的事物,最后空闲下来继续处理演讲稿;而 parallelism 指的是让两个人去做这两件事
- 这也说明,如果是一些 cpu bound 的任务,通常 parallel threads 可以增进不少效率;而如果是一些 io bound 的任务,比 thread 更有效的 fiber 会完成得更好
boost.fiber 的实现
如果单看 boost::fibers::fiber 的 API,我们会发现它与 thread(如 std::thread)非常的接近
- fiber 构造函数与线程类似,析构一个尚未结束的 fiber 也需要 join / detache(为啥没提供个 jfiber?)
- 可以通过一个枚举类 launch 指定该构造的 fiber 是等候 scheduler 调度才执行(post,默认)还是取代 caller fiber(dispatch)立即执行
- 可以通过 get_id 方法拿到 fiber 的 id
- boost::this_fiber 这个 namespace 提供了一些 utility,比如 get_id、sleep_for/_until、yield(这会将当前 fiber 挂起)
提供了几个简单的 scheduling 算法(round_robin、work_stealing、shared_work),这可以使用 boost::fibers::use_scheduling_algorithm<>() 模板设置,他们都实现了 boost::fibers::algo::algorithm 接口。
需要注意的是同步工具不能与 thread 混用,因为线程的同步往往是跨 core 的,而 fiber 是在同一个 core 上执行的。
- boost::fibers::mutex 提供了互斥锁,尽管多数情况下对某些共享资源的访问是 exclusive 的,还是存在需要保护 critical resource 不被几个 fiber 访问
- boost::fibers::condition_variable(_any) 提供了与 mutex 一起使用方便检查数据 availability 的情况
- boost::fibers::barrier 使得多个 fiber 能“同时”被唤醒(其实是一个接一个)
- channel 是一个更好用的数据分享机制(incremental),它是一个 typed queue(又细分为 buffered 与 unbuffered,后者在没有 producer / consumer 任意一方的时候写入或者读出都会导致 block),方便一个 fiber 将数据写入,另一个将数据读出来
- future / promise 与 packaged_task 与线程的 API 类似
如果没理解错,fiber 调用 blocking IO 后,scheduler 是可以执行另外的 fiber 的,那对于一些其他的情况
- 一个 async API 通常通过一个 api + callback(数据 ready 后如何处理),但这个 API 会立即返回(看有没有 error),因此常见的做法是使用 promise / future 结构
- 某些 non-blocking io 的 API 是理解返回结果或者 EWOULDBLOCK(错误,表示这个时候拿不到),可以用一个 while loop 来 poll,失败了通过 yield 让其他 fiber 有机会执行
如果还有另一个 event manager / system(常见的如 UI 系统会有一个自己的 event loop),我们需要在它的 main loop 中调用 this_fiber::yield 来寻求一个给 fiber scheduler 执行的机会。
一个例子
我们这里仅仅想看看调用一个 blocking call 的情况是否会导致 fiber 的挂起。
#include <boost/fiber/all.hpp>
#include <chrono>
#include <iostream>
#include <string_view>
using namespace std::chrono_literals;
void exp1_work(std::string_view msg) {
std::cout << "Experiment 1: entering " << msg << "\n";
for (int i = 0; i < 3; ++i) {
std::cout << "Fiber id = " << boost::this_fiber::get_id() << ": started "
<< i << "th time\n";
boost::this_fiber::sleep_for(200ms);
}
std::cout << "Experiment 1: leaving " << msg << "\n";
}
void exp1() {
std::cout << "Experiment 1\n";
std::cout << "Experiment 1: firing up fibers, has_ready_fibers = "
<< boost::fibers::has_ready_fibers() << "\n";
boost::fibers::fiber f1(boost::fibers::launch::post, exp1_work, "foo");
std::cout << "Experiment 1: fiber 1, joinable = " << f1.joinable() << "\n";
boost::fibers::fiber f2(boost::fibers::launch::post, exp1_work, "bar");
std::cout << "Experiment 1: fiber 2, joinable = " << f2.joinable() << "\n";
std::cout << "Experiment 1: joining\n";
f1.join();
f2.join();
std::cout << "Experiment 1: ending\n";
}
void exp2() {
std::cout << "Experiment 2\n";
std::cout << "Experiment 2: firing up fibers, has_ready_fibers = "
<< boost::fibers::has_ready_fibers() << "\n";
boost::fibers::fiber f1([]() {
std::cout << "Experiment 2: entering f1 before sleep\n";
sleep(1);
std::cout << "Experiment 2: leaving f1\n";
});
boost::fibers::fiber f2([]() {
boost::this_fiber::yield(); // make sure f1 goes first
std::cout << "Experiment 2: entering f2 after first yield\n";
boost::this_fiber::yield(); // make sure f1 goes first
std::cout << "Experiment 2: f2 will not run before f1 end\n";
});
std::cout << "Experiment 2: fiber 2, joinable = " << f2.joinable() << "\n";
std::cout << "Experiment 2: joining\n";
f1.join();
f2.join();
std::cout << "Experiment 2: ending\n";
}
int main(int argc, char* argv[]) {
exp1();
exp2();
return 0;
}
我这里实在是 mac 里面编译的,因为 boost 是通过系统的 clang 编译的,似乎这里不能使用 gcc 编译连接,比如编译后找不到 sleep_for 对应的二进制(库里面也许默认的某些东西不一样?比如 chrono 默认实现的类名),即便跳过这个有问题的函数,一到 fiber 的构造函数也会崩溃。换回 clang 后正常,编译命令为
c++ -std=c++20 -I/opt/local/libexec/boost/1.81/include -L/opt/local/libexec/boost/1.81/lib -lboost_context-mt -lboost_fiber-mt app.cpp -o app
执行结果
Experiment 1
Experiment 1: firing up fibers, has_ready_fibers = 1
Experiment 1: fiber 1, joinable = 1
Experiment 1: fiber 2, joinable = 1
Experiment 1: joining
Experiment 1: entering foo
Fiber id = 0x7fddc0047f00: started 0th time
Experiment 1: entering bar
Fiber id = 0x7fddc0067f00: started 0th time
Fiber id = 0x7fddc0047f00: started 1th time
Fiber id = 0x7fddc0067f00: started 1th time
Fiber id = 0x7fddc0047f00: started 2th time
Fiber id = 0x7fddc0067f00: started 2th time
Experiment 1: leaving foo
Experiment 1: leaving bar
Experiment 1: ending
Experiment 2
Experiment 2: firing up fibers, has_ready_fibers = 1
Experiment 2: fiber 2, joinable = 1
Experiment 2: joining
Experiment 2: entering f1 before sleep
Experiment 2: leaving f1
Experiment 2: entering f2 after first yield
Experiment 2: f2 will not run before f1 end
Experiment 2: ending
需要注意的是一个系统级的 api(如 sleep)因为并没有 fiber aware,因此不可能在这里面给 f2 让路,同理如果我们使用 fopen/fread 这些文件 IO,也会有类似的问题。我们需要在 boost.asio 里面重新审视一下如何利用 fiber / coroutine 等用户级别可控的 concurrency。