目录
实现简单的"纤程"
好长时间没有更新博客了。我最近读了 Implementing simple cooperative threads in C 这篇文章。它说的 cooperative threads,实际上就是每一个“线程”或者说控制流,可以主动的让出 CPU 的使用权,来达成某种意义上的“合作”。这样的行为似乎很接近纤程/用户态线程,所以我在这里姑且翻译成了“纤程”。读完之后,我用 C++ 又重新实现了一下。很久没有写 C++ 了,写得磕磕绊绊的,源代码在这里。
接口
这篇文章描述的实现思路很简单,它尝试用一个数据结构去描述一个任务执行的上下文,并使用一个队列来维护没有完成的任务。任务以及调度器的接口如下:
1 | class TaskHolder { |
只看上面的接口可能有点抽象,下面给出一个实际的使用例子:
1 | scheduler::SequentialScheduler scheduler{}; |
上述代码的输出是:
1 | Task 1: 0 |
实现原理
和原文的实现一样,我的实现包含了两个核心部分:
- 使用
setjmp
和longjmp
来实现控制流的转移,保留上下文,即所有寄存器的值。 - 自行维护每个任务的栈内存,保留栈空间。
setjmp
和 longjmp
setjmp
和 longjmp
提供了类似汇编中 jmp
指令的功能. setjmp
可以将当前指令的内存地址存储一个数据结构中(jmp_buf
),而 longjmp
可以返回 jmp_buf
指定的地址继续执行。下面我们看一个例子。
1 | jmp_buf target; |
上面这段代码会进入一个死循环,在输出一次 Set up a place for jumping 后会不断地输出 Someone jumps here! With a value 100。之所以有这样行为的原因主要是:
longjmp
会跳回setjmp
发生的位置重新执行。setjmp
实际调用会返回 0,如果由longjmp
调用则会返回longjmp
的第二个参数值。
手动维护栈内存
大家的知道,在执行代码的过程中,为了保证每个线程才能互不干扰的执行自己的逻辑,它们需要有独立的栈空间,操作系统会保证这一点。在我们现在的实现中,每个任务也需要有独立的栈内存。没有了操作系统的帮助,我们需要自己手动分配内存,并将其指定为栈内存。核心的实现代码如下:
1 | // 分配栈内存 |
总结
实现代码见 Github,这里就不再赘述了。
可能的扩展
Github 上给出了 SequentialScheduler
的实现,是一个单线程的版本。我感觉可以比较简单的扩展成可并发的版本。有兴趣的朋友可以试一下。
为什么用纤程这个词
我最开始在纤程和协程中犹豫了一下。
根据我个人的理解,协程之间应该有显式的控制流切换。而在我们的实现中,只有 task 和 Scheduler
之间存在控制流切换,因此我觉得协程不太恰当。
我们的实现,某种程度上似乎很像 event loop。最大的区别在于,我们并不需要一个事件来决定哪一个任务是可调度的。我的实现中,每一个任务都是可以调度的。
如果我们扩展成了可并发的版本,也许使用纤程就比较恰当了。