区分协程与纤程
文档翻译自N4024-open-std
概观
本文的目的是指出即将提出的将纤程引入C ++标准库的建议; 简要描述所提出的纤程库中的特征; 并将其与N39856 [^6] 中提出的协程库进行对比。
希望这种比较有助于澄清提出的协程库的特征集。 某些特征正确地属于协程库; 其他概念相关的功能更适合属于纤程库。
背景
一个协程库最初是在N37084[^4]中提出的; 该提案随后在N39856中进行了修订。 随后的一些讨论表明,为了完善协程提议,这可能对作者消除纤程库的概念空间和协程库的概念空间中的歧义来说很有用。
纤程
纤程库的速写
就本文而言,我们可以将术语“纤程”视为“用户空间线程”。纤程启动后,理论上可以有一个独立于启动它代码的生命周期。纤程可以从启动代码中分离出来;或者,一个纤程可以连接另一个。纤程可以睡眠,直到指定的时间或特定的持续时间。多个概念独立的纤程可以在同一内核线程上运行。当纤程阻塞,例如等待尚不可用的结果时,同一线程上的其他纤程会继续运行。 “阻塞”纤程隐式地将控制权转交给纤程调度器,以分派一些其他的马上可以使用的纤程。
纤程在概念上与内核线程相似。实际上,即将推出的纤程库建议有意模仿std::thread API的大部分内容。它提供纤程本地存储。它提供了几种不同的纤程mutex。它提供了condition_variables和barriers。它提供有界和无界的队列。它提供feature,shared_future,promise和packaged_task。这些面向纤程的同步机制不同于它们的线程,因为当(比如说)一个mutex阻塞了它的调用者时,它只会阻塞调用它的纤程 - 而不是这个纤程所在运行的整个线程。
当纤程阻塞时,它不能假定调度器会在它的等待条件满足的时候唤醒它。满足这种条件标志着等待中的纤程准备就绪;最终调度器对选择准备好的纤程进行调度。
纤程和内核线程之间的主要区别在于纤程使用协作式上下文切换,而不是预先分割时间片。同一内核线程上的两个纤程不能同时在不同的处理器内核上运行。在特定内核线程中,任何时刻最多只有一个纤程在运行。
这有几个含义:
纤程上下文切换不涉及内核:它完全发生在用户空间中。这使得纤程实现切换上下文的速度明显快于线程上下文切换。
同一线程中的两个纤程不能同时执行。这可以极大地简化这些纤程之间的数据共享:同一个线程中的两个纤程不可能相互竞争。因此,在特定线程的区域内,不需要锁定共享数据。
纤程的编程者必须小心地将自发的上下文切换分散到长时间的CPU绑定操作中。由于纤程环境切换是完全协作式的,如果没有这种预防措施,纤程库不能保证每根纤程的运行。
一个调用阻塞调用者线程的标准库或操作系统函数的纤程实际上将阻塞其运行的整个线程,包括同一线程上的所有其他纤程。纤程的编程者必须注意使用异步 I/O 操作,或使用纤程阻塞而不是线程阻塞的操作。
实际上,纤程扩展了并发分类:
- 在单台计算机内,可以运行多个进程
- 在单个进程内,可以运行多个线程
- 在单个线程内,可以运行多个纤程。
一些纤程使用案例
当您想要启动一个(可能很复杂的)异步I / O操作序列时,特别是当必须根据其结果进行迭代或决策时,纤程很有用。
单一的纤程可用于执行并发异步读取操作,将其结果汇总到纤程专用队列中供其他纤程使用。
纤程对于在事件驱动程序中组织响应代码很有用。通常情况下,这样的程序中的事件处理程序不能阻塞调用它的线程:这会停顿所有其他事件(如鼠标移动)的处理程序。处理程序必须使用异步 I/O 而不是阻塞 I/O。纤程允许处理程序在完成异步 I/O 操作后恢复,而不是将后续逻辑分解为完全不同的处理程序。
可以使用纤程来实现任务处理框架来解决C10K-问题 [^1]
例如严格的fork-join任务并行 [^5] 支持两种风格 - fully-strict computation [^5] (没有任何任务可以继续,直到它join它的所有子任务)和 terminally-strict computations[^5] (子任务只在处理结束时才连接) 。
此外,不同的调度策略也是可能的: work-stealing和continuation-stealing。
对于 work-stealing,调度器创建一个子任务(子纤程)并立即返回给调用者。根据可用资源(CPU等),每个子任务(子级纤程)可以执行或者被调度器“窃取”。
对于continuation-stealing,调度程序立即执行生成的子任务(子级纤程)。由于资源可用,剩余的功能(续)被调度程序“窃取”。
协程
协程库的快速回顾
协程被实例化并被调用。当调用者调用协程时,控制立即转入该协程;当协程yield时,控制立即返回给其调用者(或者在对称协程的情况下,返回给下一个协程)。
协程没有独立于调用者的概念性生命周期。调用代码实例化一个协程,并将控制权来回传递一段时间,然后销毁它。说“挂起”协程是没有意义的。说“阻塞”协程也是没有意义的:协程库不提供调度器。协程库不提供同步协程的工具:协程已经是同步的。
协程不像线程。一个协程更接近于一个具有语义扩展的普通函数:将控制传递给调用者,期望在稍后恢复到完全相同的点。当调用者恢复一个协程时,控制转移是立即的。没有中介,也没有代理决定下一步恢复哪个协程。
一些协程使用案例
通常情况下,当消费者代码调用生产者函数来获取一个值时,生产者必须返回消费者一个值,忽略其所有本地状态。 协程允许你编写生产者代码,(通过函数调用)将值推送给使用函数调用拉取它的消费者。
例如,协程可以应用回调(如从SAX解析器)到消费者显式请求的值。
此外,所提出的协程库提供了生成器协程上的迭代器,因此来自生产者的一系列值可以直接传递到STL算法中。 例如,这可以用来使树形结构扁平。
协程可以链接在一起:协程源可以通过一个或多个过滤协程提供值,然后这些值最终传递给消费者代码。
在所有上面的例子中,正如每个协程的使用一样,生产者和消费者之间的握手是直接而没有间隔的。
关系
作者提供了即将推出的纤程库(boost.fiber[^3])的参考实现。参考实现完全用可移植的C ++编码;实际上它的原始实现完全在C ++ 03中。
这是可能的,因为纤程库的参考实现建立在提供上下文管理的boost.coroutine[^2]上。纤程库通过添加调度器和上述同步机制来扩展协程库。
当然,也可以在纤程上实现协程。但是就协程而言,这些概念更加巧妙地映射到实现纤程。相应的操作是:
- 协同yield;
- 纤程阻塞。
当协程yield时,它将控制直接传递给其调用者(或者在对称协程的情况是一个特定其他协程)。
当纤程阻塞时,它会将控制隐式传递给纤程调度器。协程没有调度程序,因为它们不需要调度程序。
总结
希望本文能够帮助阐明所提出的协程库中的一些已知遗漏,以便读者将所需的功能与协程库方案和即将推出的纤程库方案联系起来
References
[^1]: The C10K problem, Dan Kegel
[^2]: boost.coroutine
[^3]: boost.fiber
[^4]: N3708: A proposal to add coroutines to the C++ standard library
[^5]: N3832: Task Region
[^6]: N3985: A proposal to add coroutines to the C++ standard library (Revision 1)