什么是协程?
协程介绍
协程(coroutine)是计算机程序的一类组件,类似于线程,是一种用于处理协作式多任务的方式。
协程适合于用来实现彼此熟悉的程序组件,如协作式多任务、异常处理、事件循环、迭代器、无限列表和管道。
协作意味着由拥有控制权的任务决定什么时候交出控制权,且交给哪个任务,而不是由操作系统的调度内核决定。
和线程的区别
线程使用通用的多任务执行方式,其依赖于操作系统内核的实现并行和并发(看上去同时执行,且实际可在多个 CPU 同时运行)的多任务处理,而协程是应用/语言级别(在协程之间的切换不需要涉及任何系统调用或任何阻塞调用)的多任务处理,无需依赖操作系统,这意味着协程提供并发性(看上去是同时执行,实际为交替执行)而非并行性。
协程不需要用来守卫关键区块的同步性原语(primitive)比如互斥锁、信号量等,并且不需要来自操作系统的支持。协程是语言层级的构造,可看作一种形式的控制流,而线程是系统层级的构造。
协程示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var q := new 队列
coroutine 生产者
loop
while q 不满载
建立某些新产品
向 q 增加这些产品
yield 消费者
coroutine 消费者
loop
while q 不空载
从 q 移除某些产品
使用这些产品
yield 生产者
协程可以通过 yield
(理解为让步)来主动让出执行流控制权并调用其它协程,接下来的每次协程被调用时,从协程上次 yield 返回的位置接着执行。
通过 yield 方式转移执行权的协程之间不是调用者与被调用者的关系,而是彼此对称、平等的。
协程实例
在《UML 状态图的实用 C/C++设计》(QP 状态机)学习笔记中提到的合作式 QV 内核就是类似协程的概念,其不依赖于操作系统,即使在裸机环境也可执行。每个任务执行一定步骤主动交出控制权来实现并发处理,对于 QP 这种事件驱动型状态机来说,这种调度方式非常合适。
生成器generator
生成器函数看起来和普通函数类似,但它使用 yield 而不是 return 来返回值。每次调用生成器的 __next__()
方法(或者通过 next() 函数),它会从上次离开的地方继续执行,直到遇到下一个 yield。
1
2
3
4
5
6
7
8
9
10
11
def my_generator():
yield 1
yield 2
yield 3
# 使用生成器
gen = my_generator()
print(next(gen)) # 输出 1
print(next(gen)) # 输出 2
print(next(gen)) # 输出 3
生成器是迭代器的一种,所以它实现了迭代器的协议,即 __iter__()
和 __next__()
方法。你可以使用 for 循环来遍历生成器。
yield
是生成器的核心。每次执行 yield 时,生成器会暂停其状态,保留局部变量的值、当前的执行位置等信息。下一次调用 __next__()
或 next()
时,生成器会从暂停的地方继续执行。
python:
1
2
3
4
5
6
7
8
9
def fibonacci(limit):
a, b = 0, 1
while a < limit:
yield a
a, b = b, a + b
# 生成 10 以内的 Fibonacci 数列
for value in fibonacci(10):
print(value)
C++ 23:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <generator>
#include <utility>
#include <iostream>
std::generator<uint64_t> fib(int max) {
auto a = 0, b = 1;
for (auto n = 0; n < max; n++) {
co_yield std::exchange(a, std::exchange(b, a + b));
}
}
int main(){
for(auto&& i: fib(10)) { // 输出fibonacci数列的前十个
std::cout<<i<<"\n";
}
}
C++ 20:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
#include <coroutine>
#include <iostream>
#include <utility>
template <typename T> struct Generator {
// 实现一个协程需要一个 promise_type 用于状态管理。名字必须叫promise_type
struct promise_type {
T value_;
// 返回一个协程对象,它持有协程的句柄。编译器会自动调用
Generator get_return_object() {
return Generator{
std::coroutine_handle<promise_type>::from_promise(*this)};
}
// 定义协程的初始挂起行为。通常返回
// std::suspend_always,表示协程初始时会挂起。
static std::suspend_always initial_suspend() { return {}; }
// 定义协程的最终挂起行为。通常返回
// std::suspend_always,表示协程结束时会挂起,以便进行清理。
static std::suspend_always final_suspend() noexcept { return {}; }
// 用于返回一个值并挂起协程。
template <std::convertible_to<T> From> // C++20 concept
std::suspend_always yield_value(From &&from) {
value_ = std::forward<From>(from); // caching the result in promise
return {};
}
// 用于协程正常结束时的处理。
void return_void() {}
void unhandled_exception() { std::terminate(); }
};
// 用于恢复协程的执行、检查协程的状态等。它是一个轻量级的对象,提供了协程的生命周期管理。
std::coroutine_handle<promise_type> coro;
Generator(std::coroutine_handle<promise_type> h) : coro(h) {}
~Generator() {
if (coro)
coro.destroy();
}
bool move_next() {
coro.resume();
return !coro.done();
}
T current_value() const { return coro.promise().value_; }
};
Generator<int> fibonacci() {
int a = 0, b = 1;
while (true) {
// 相当于co_await promise.yield_value(expr)。
// 第一次调用时,会调用Generator构造函数,找到该类中名为promise_type的内部类并构造,然后调用其get_return_object
// 生成一个std::coroutine_handle<promise_type>作为Generator的参数
co_yield std::exchange(a, std::exchange(b, a + b));
}
}
int main() {
auto fib = fibonacci();
for (int i = 0; i < 10; ++i) { // 生成前 10 个斐波那契数
fib.move_next();
std::cout << fib.current_value() << " ";
}
}