Coroutine

2024-09-11

最后编辑于:2024-09-15

    #Cpp20
    #coroutine
    #Cpp

Coroutine

C++20的协程为非对称协程(Asymmetric Coroutine)。其中协程只能通过一个特殊的操作(通常是yield)来将控制权交还给调用者(协程上下文)。 非对称协程的执行始终由调用者主动开始或恢复,然后协程在合适的时机退出,交还控制权,调用者运行调度算法再来决定是否继续运行或结束协程。

协程的基本概念

  • 承诺对象(promise_type),在协程内部操控。协程通过这个对象提交结果或异常
  • 协程句柄(coroutine handle),在协程外部操作。用于恢复协程执行或销毁协程帧
  • 协程状态(coroutine state),分配于堆上的内部对象,包含: 承诺对象 参数(copied by value) 挂起点 当前挂起点的局部变量和临时值,,状态信息和堆栈指针

协程函数的执行流程

  • 调用协程函数:构建协程框架和 promise_type 对象。
  • 执行到 initial_suspend:确定协程是否挂起。
  • 协程体执行:协程主体开始执行,直到第一个 co_awaitco_yield
  • 挂起协程:在挂起点,协程暂停并返回控制权给调用者。
  • 恢复协程:调用者可以通过句柄恢复协程,继续执行。
  • 协程结束:当协程完成时,执行 final_suspend,决定如何结束和清理。
  • 销毁协程:协程销毁后释放框架和资源。

协程函数需要一个特殊的返回值类型(以Result为例),其中需要定义一个承诺对象(必须为Result::promise_type的形式,且名为promise_type),承诺对象需要实现一些必须的接口

#include <coroutine>

struct Result {
	struct promise_type {
		...
	};
	std::coroutine_handle<promise_type> handle;
};

// 或者

struct promise;

struct Result {
	using promise_type = promise;
	std::coroutine_handle<promise_type> handle;
};

struct promise {
	...
};

协程的关键字

co_awaitco_yield可以挂起协程,co_return退出协程

co_await返回一个awaiter对象,可以对挂起过程中的一些细节进行控制 co_yield返回一个用户定义值,你可以在promise对象中储存这个值,并使用handle.promise().value来进行访问,例如Task的寄存器上下文

coroutine task() {
	// do some tasks
	co_await std::suspend_always {};
}

编译器会做的

corountine task() {
	corourtine::promise_type p();
	coroutine co_object = p.get_return_object();

	try {
		co_await p.initial_suspend();
		
		// do some tasks
		co_await std::suspend_always {};
	} catch(...) {
		p.unhandled_exception();
	}

	co_await p.final_suspend();
}

awaiter

展开编译器的对于co_await的实现

co_await std::suspend_always {};
// 之前构造的promise和coroutine object
corourtine::promise_type p();
coroutine co_object = p.get_return_object();

auto&& awaiter = std::suspend_always {};
if (!awaiter.await_ready()) {
	awaiter.await_suspend(p.handle);
	// leave here 
}
// resume here
awaiter.resume();

await_ready(std::coroutine_handle<>)

awaiter可以理解为在co_await准备挂起协程时,提供了一个接口await_readyready指的是异步操作是否完成,来让你可以对异步操作的状态做一次检查,若异步操作在运行到此处时已经执行完成,则可以放弃挂起。这样避免了立即完成的异步操作会被直接挂起,需要等待下一次调度才能恢复

struct awaiter {
	bool await_ready() {
		return check_task_state();
	}
};

await_suspend()

协程在决定要挂起前,提供的一个接口,让你可以在挂起协程前做一些准备,例如,将协程句柄注册到回调、任务调度器或其他的一些异步管理机制中

总是挂起

void await_suspend(std::coroutine_handle<>)

返回true时挂起

bool await_suspend(std::coroutine_handle<>) 

所以一个协程是否挂起需要 await_ready()await_suspend() 的返回值共同决定。

  • await_ready():是第一步,用于决定是否直接跳过挂起过程。如果 await_ready() 返回 true,表示异步任务已经准备好,协程不需要挂起,直接继续执行,不会调用 await_suspend()
  • await_suspend():是第二步,如果 await_ready() 返回 false,协程就会检查 await_suspend() 来决定是否挂起。await_suspend() 返回 true 时,协程挂起;如果返回 false,协程会立即恢复,不会挂起。

挂起并恢复返回的协程

std::coroutine_handle<> await_suspend(std::coroutine_handle<>)

Coroutine Handle

使用coroutine_handle<>可以表示任意协程句柄 在知道协程的类型时,promise对象、协程句柄和raw address可以进行转换

promise <> handle

handle = std::coroutine_handle<promise_type>::from_promise(promise);
promise = handle.promise();

handle <> raw address

address = handle.address();
handle = std::coroutine_handle<promise_type>::from_address(address);

协程句柄还提供在外部控制协程的一些接口

恢复协程

handle.resume();

检查协程完成状态

handle.done();

销毁句柄

handle.destroy();

参考

CppNow2023 一篇文章搞懂 C++ 20 协程 Coroutine cppreference