协程简介

协程是一个能在执行过程中被挂起、随后又能恢复执行的函数。
c++20中使用的是无栈协程,对协程的定义为:一个函数如果包含co_awaitco_yieldco_return三个关键字中的任意一个,则被称为协程。

co_await关键字

co_await是一个一元操作符,后面跟一个表达式expr,能挂起一个协程并将控制权返回给调用方。这里的expr需要返回一个awaitable对象,awaitable对象内部定制了当前协程是否需要挂起等操作。

awaitable对象

定义一个awaitable对象,主要需要实现三个函数:

  • await_ready(): 在被co_await时是否已经准备好了,如果返回false,那么co_await awaitable就会立即暂停该协程,然后调用awaitable.await_suspend()。如果返回true,说明数据已经准备好了,那么就会继续执行该协程。
  • await_suspend():协程被挂起,该函数就会被调用。该函数返回voidtrue,协程挂起,将控制权交给调用者;如果返回false,则会立即恢复该协程;如果返回其他协程句柄,则立即恢复那个协程。
  • await_resume():当协程句柄的resume()函数被调用,则该函数被调用。该函数的返回值就是co_await awaitable的返回值。

协程库中提供了两个awaitable对象:

  • suspend_always;表示co_await expr需要挂起,不产生值。await_suspendawait_resume都为空,await_ready直接返回false
  • suspend_never:表示co_await expr不需要挂起。await_ready直接返回true

一个awaitable对象的简单定义:

class MyAwaitable {
public:
MyAwaitable() = default;

bool await_ready() {
std::cout << "await ready" << std::endl;
return false;
}
void await_suspend(std::coroutine_handle<> handle) {
std::cout << "await suspend" << std::endl;
}
void await_resume() {
std::cout << "await resume" << std::endl;
}
};

协程对象

现在我们有个函数,函数内部包含co_await,这就是一个协程,但如果想操作这个协程,我们仍需要定义一个协程对象,用来操作该协程和获取协程状态。
一个协程对象要和一个承诺对象(promise object)关联,承诺对象由协程对象操作,协程通过承诺对象提交结果或执行异常处理,协程对象由调用者操作。
承诺对象至少需要实现五个函数:

  • get_return_object():获取协程对象。我们可以通过std::coroutine_handle::from_promise()获取关联的协程句柄。
  • initial_suspend():该函数返回一个awaitable对象,协程在初始化时会co_await它,对于惰性启动的协程,可以返回std::suspend_always,对于立即启动的协程,可以返回std::suspend_never
  • final_suspend():返回一个awaitable对象,如果返回suspend_never,程序结束时不会挂起,会释放相关资源。如果返回suspend_always,协程结束时挂起,调用者仍可以从协程对象中获取相关资源。注意,此时恢复该协程会发生未定义的行为。该函数必须是noexcept的,不能引发异常。
  • unhandled_exception():当协程因异常结束,调用该函数处理异常。
  • reture_void()/reture_value():和co_reture关键字相关,当co_return;不携带返回值,调用reture_void,协程函数末尾会隐含一个co_returnco_reture expr;携带返回值,调用return_value

一个简单协程对象的定义:

class MyCoroutine {
public:
class MyPromise {
public:
MyCoroutine get_return_object() {
std::cout << "get coroutine handle" << std::endl;
return std::coroutine_handle<MyPromise>::from_promise(*this);
}
std::suspend_never initial_suspend() {
std::cout << "initial suspend" << std::endl;
return {};
}
std::suspend_never final_suspend() noexcept {
std::cout << "final suspend" << std::endl;
return {};
}
void unhandled_exception() {

}
void return_void() {
std::cout << "return void" << std::endl;
}
};

using promise_type = MyPromise;
MyCoroutine(std::coroutine_handle<MyPromise> handle) : _handle(handle) {}

void resume() {
_handle.resume();
}

private:
std::coroutine_handle<MyPromise> _handle;
};

一个协程的运行过程

我们利用上面实现的awaitable对象和协程对象来实现一个完整的协程,观察协程的运行过程。

#include <iostream>
#include <coroutine>

class MyCoroutine {
public:
class MyPromise {
public:
MyCoroutine get_return_object() {
std::cout << "get coroutine handle" << std::endl;
return std::coroutine_handle<MyPromise>::from_promise(*this);
}
std::suspend_never initial_suspend() {
std::cout << "initial suspend" << std::endl;
return {};
}
std::suspend_never final_suspend() noexcept {
std::cout << "final suspend" << std::endl;
return {};
}
void unhandled_exception() {

}
void return_void() {
std::cout << "return void" << std::endl;
}
};

using promise_type = MyPromise;
MyCoroutine(std::coroutine_handle<MyPromise> handle) : _handle(handle) {}

void resume() {
_handle.resume();
}

private:
std::coroutine_handle<MyPromise> _handle;
};

class MyAwaitable {
public:
MyAwaitable() = default;

bool await_ready() {
std::cout << "await ready" << std::endl;
return false;
}
void await_suspend(std::coroutine_handle<> handle) {
std::cout << "await suspend" << std::endl;
}
void await_resume() {
std::cout << "await resume" << std::endl;
}
};

MyCoroutine test_my_coroutine() {
std::cout << "start my coroutine" << std::endl;
co_await MyAwaitable{};
std::cout << "end my coroutine" << std::endl;
}

int main() {
MyCoroutine my_routine = test_my_coroutine();
my_routine.resume();
return 0;
}

上述样例返回

get coroutine handle
initial suspend
start my coroutine
await ready
await suspend
await resume
end my coroutine
return void
final suspend

上面样例中我们创建了一个MyCoroutine的协程,整个协程运行流程为:

  1. 首先test_my_corotine()这个函数已经变成了一个协程,协程在运行前会先创建一个协程对象MyCorotine,开始执行承诺对象MyPromise中相关方法,调用get_return_object返回协程句柄。
  2. 然后调用initial_suspend,由于返回的是suspend_never,所以不会挂起协程,直接开始执行协程。
  3. 在执行到co_await MyAwaitable处停止,执行MyAwaitable中相关函数,先执行await_ready方法,返回的是false,协程需要挂起。
  4. 协程挂起,执行await_suspend,控制权返回给main函数。
  5. 协程调用resume方法,协程重新执行,MyAwaitable对象执行await_resume
  6. 继续执行协程函数。
  7. 协程函数默认调用co_return,协程对象中承诺对象执行reture_void
  8. 协程执行完,最后承诺对象调用final_suspend

co_return关键字

协程的返回操作,协程执行到最后会默认添加一个co_returnco_return调用会触发承诺对象调用return_void,也可以主动调用co_return expr返回一个值,会触发调用承诺对象调用return_value(expr)

co_yield关键字

co_yield返回一个值给调用者,同时挂起当前的协程。它实际是co_await的语法糖形式:

co_await promise.yield_value(expr)

如果想使用co_yield,我们需要给承诺对象提供一个yield_value函数,并且该函数返回一个awaitable对象,这个语法通常用来实现一个惰性生成器。

斐波拉契数列生成器

下面实现了一个斐波拉契数列生成器:

#include <iostream>
#include <coroutine>

class FiboCoroutine {
public:
class FiboPromise {
public:
FiboCoroutine get_return_object() {
return std::coroutine_handle<FiboPromise>::from_promise(*this);
}
// 由于是惰性生成器,我们在协程初始化时就暂停,所以返回std::suspend_always
std::suspend_always initial_suspend() {
return {};
}
std::suspend_never final_suspend() noexcept {
return {};
}
void unhandled_exception() {

}
void return_void() {
std::cout << "return void" << std::endl;
}

std::suspend_always yield_value(size_t value) {
_value = value;
std::cout << "yield value: " << value << std::endl;
return {};
}

size_t _value = 0;
};

using promise_type = FiboPromise;
FiboCoroutine(std::coroutine_handle<FiboPromise> handle) : _handle(handle) {}

size_t get_val() {
_handle.resume();
return _handle.promise()._value;
}

private:
std::coroutine_handle<FiboPromise> _handle;
};

FiboCoroutine get_fibo() {
co_yield 1;
co_yield 1;
size_t p1 = 1;
size_t p2 = 1;
size_t p3 = 0;
for(int i = 0; ; i++){
p3 = p1 + p2;
co_yield p3;
p1 = p2;
p2 = p3;
}
}

int main() {
FiboCoroutine fibo = get_fibo();
for(int i = 0; i < 10; ++i){
size_t val = fibo.get_val();
std::cout << "fibo " << i << " " << val << std::endl;
}
return 0;
}

这里需要注意承诺对象中的init_suspend返回suspend_always

参考