C++20 协程coroutine

1.协程概念

协程函数与普通函数的区别:

(1)普通函数执行完返回,则结束。
协程函数可以运行到一半,返回并保留上下文;下次唤醒时恢复上下文,可以接着执行。

协程与多线程:

(1)协程适合IO密集型程序,一个线程可以调度执行成千上万的协程,IO事件不会阻塞线程
(2)多线程适合CPU密集型场景,每个线程都负责cpu计算,cpu得到充分利用

协程与异步:

(1)都是不阻塞线程的编程方式,但是协程是用同步的方式编程、实现异步的目的,比较适合代码编写、阅读和理解
(2)异步编程通常使用callback函数实现,将一个功能拆分到不同的函数,相比协程编写和理解的成本更高。
(3)个人觉得异步编程都可以改成协程。

2.关键字

C++20引入了三个新的关键字 co_await, co_yield, co_return实现协程。包含这三个关键字的函数就是协程函数。

co_await :用来暂停和恢复协程的, 返回awaitable类型.
co_yield :用来暂停协程并且往绑定的 promise类型 里面 yield 一个值.
co_return :往绑定的 promise类型 里面放入一个值.

这里提到了2个概念:promise和awaitable。这2个概念比较重要,理解这2个概念后才能定制协程的流程和功能。

3.promise和awaitable类型

(1)promise和awaitable类型都是“鸭子类型”,不是继承具体的基类,而是实现了各自要求的api接口的就是promise类型或awaitable类型

(2)最简单的awaitable类型需要实现3个函数:
1)await_ready:返回类型bool,表示awaitable实例是否已经ready。协程开始会调用此函数,如果返回true,表示你想得到的结果已经得到了,协程不需要执行了。如果返回false,本协程就会挂起。
2)await_suspend:可以选择返回 void , bool , std::coroutine_handle

(P为本协程promise类型,为void时可以不写)之一。挂起awaitable。该函数会传入一个coroutine_handle类型(标识本协程)的参数handle,这是一个由编译器生成的变量。在这个函数中控制什么时候恢复协程(通过调用handle.resume())。

i> 如果 await_suspend 返回类型是 std::coroutine_handle, 那么就会恢复这个 handle. 即运行 await_suspend(hanle).resume(). 这意味着暂停本协程的时候, 可以恢复另一个协程
ii> 如果 await_suspend 返回类型是 bool, 那么看返回结果, 是 false 就恢复自己.
iii> 如果 await_suspend 返回类型是 void, 那么就直接执行. 执行完暂停本协程

3)await_resume:返回值就是co_await运算符的返回值。当协程重新运行时,会调用该函数。

系统库已经提供了std::suspend_never, std::suspend_always可供调用。当然也可以自己实现定制的awaitable类型,例如:

struct Action{											// 名称任意
	bool await_ready() noexcept{return false;}			// 必须实现此接口
	void await_suspend(coroutine_handle<>) noexcept	{}	// 必须实现此接口, 可通过此处在函数内部获取到handle
	void await_resume() noexcept{}						// 必须实现此接口
}

(3)最简单的promise类型需要实现6个函数:

get_return_object // to create return object
initial_suspend // entering the coroutine body
return_value // called when co_return called
return_void // called before the end of coroutine body
yield_value // called when co_yield called
final_suspend // called when coroutine ends
unhandled_exception // handle exception

4.编译器对一个协程函数做了什么:

例如一个简单的协程函数:

Generator f()
{
    co_yield 1;
}

要想编译通过,返回值类型Generator中必须包含一个名为“promise_type”的promise类型struct的定义:

struct Generator                        // 名称任意
{
    struct promise_type;                // 名称必须为promise_type
    using handle = std::coroutine_handle;
    struct promise_type
    {
        promise_type() = default();     // 非必须
        int current_value;              // 非必须, 名称任意

        auto get_return_object() { return Generator{ handle::from_promise(*this) }; }   // 必须实现此接口
        auto initial_suspend() { return std::suspend_always{}; }        // 必须实现此接口, 返回值必须为awaitable类型
        auto final_suspend() noexcept { return std::suspend_always{}; }      // 必须实现此接口, 返回值必须为awaitable类型。MSVC需要声明为noexcept,否则报错。
        void unhandled_exception() { std::terminate(); }    // 必须实现此接口, 用于处理协程函数内部抛出错误
        void return_void() {}                               // 如果协程函数内部无关键字co_return则必须实现此接口
        // void return_value() {}                           // 如果协程函数内部有关键字co_return则必须实现此接口
        auto yield_value(int value)                         // 如果协程函数内部有关键字co_yield则必须实现此接口, 返回值必须为awaitable类型
        {
            current_value = value;
            return std::suspend_always{};
        }
    };

    bool move_next() { 
        if (coro) {
            coro.resume();
            return !coro.done();    // done()函数用于返回是否处于 final_suspend 阶段
        }
        return false;
    }
    int current_value() { return coro.promise().current_value; }
    Generator(Generator const&) = delete;
    Generator(Generator&& rhs) : coro(rhs.coro) { rhs.coro = nullptr; }
    ~Generator() {
        if (coro)
            coro.destroy();
    }

private:
    Generator(handle h) : coro(h) {
    }
    handle coro;
};

我们的调用协程函数f的主函数如下:

int main()
{
    auto g = f();
    while (g.move_next())
        std::cout << g.current_value() << std::endl;
}

通过断点分析,执行步骤:
(1)先执行promise_type() 产生一个promise对象
(2)通过promise对象, 执行get_return_object(), 产生一个coroutine_name对象, 并记录handle
(3)执行initial_suspend(), 根据返回值的await_ready()返回值 判断是否立即执行协程函数, 当返回值中await_ready()返回值为ture则立即执行协程函数, 否则调用返回值的await_suspend挂起协程、跳出到主函数。
我们这里返回值是std::suspend_always,它的await_ready()始终返回false。
(4)g.move_next()中的coro.resume() 将执行权传递给协程函数:首先执行awaitable类型(initial_suspend()返回的)的await_resume函数。然后进入协程函数接上次挂起的地方继续执行
我们这里就是执行这行:co_yield 1;
(5)执行"co_yield 1"这行时,调用yield_value(val)函数,同样的,根据返回值判断是否将执行权传递给主函数。如果返回值的await_ready返回false,则调用返回值的await_suspend挂起协程,跳出到主函数。
我们这里返回的suspend_always会将执行权交给主函数。
(6)主函数此时继续执行move_next中的"return !coro.done();" 这行,因为(4)的时候执行的是"coro.resume()"
(7)coro.done()返回false,即move_next返回true。调用g.current_value获取并打印协程co_yield的值。
(8)然后和第(4)类似,通过调用g.move_next()中的coro.resume() 将执行权传递给协程函数。
因为协程函数已经执行完语句,所以准备返回,这里没有co_return,所以调用的是return_void()函数(如果有co_return,则调用的是return_value()函数)
(9)然后调用final_suspend,协程进行收尾动作,在这阶段的 coroutine_handle::done 方法为 true,caller 可以通过这个方法判断协程是否结束,从而不再调用 resume 恢复协程
根据final_suspend的返回值的await_ready判断是否立即析构promise对象,返回true则立即析构,否则不立即析构、将执行权交给主函数。
注意如果是立即析构promise对象,则后续主函数无法通过promise获得值(强行调用可能会core)。
我们这里返回的std::suspend_always的await_ready返回false。
(10)主函数执行"return !coro.done();" 这行,move_next返回false。main函数退出while循环。
(11)析构g,调用coro.destroy,结束。

上面的执行步骤的效果等同于:

{
    promise-type promise(promise-constructor-arguments); 
    try {
        co_await promise.initial_suspend(); // 创建之后 第一次暂停
        function-body // 函数体
    } catch ( ... ) {
        if (!initial-await-resume-called)
        throw; 
        promise.unhandled_exception(); 
    }

    final-suspend:
    co_await promise.final_suspend(); // 最后一次暂停
}

5.协程的储存空间

(1)C++ 的设计是无栈协程, 所有的局部状态都储存在堆上.
(2)储存协程的状态需要分配空间. 分配 frame 的时候会先搜索 promise_type 有没有提供 operator new, 其次是搜索全局范围.
(3)有分配就可能会有失败. 如果写了 get_return_object_on_allocation_failure() 函数, 那就是失败后的办法, 代替 get_return_object() 来完成工作. (需要 noexcept)
(4)协程结束以后的释放空间也会先在 promise_type 里面搜索 operator delete, 其次搜索全局范围.
(5)协程的储存空间只有在运行完 final_suspend 之后才会析构, 或者你得显式调用 coro.destroy(). 否则协程的存储空间就永远不会释放. 如果你在 final_suspend 那里停下了, 那么就得在包装函数里面手动调用 coro.destroy(), 不然就会漏内存.
(6)如果已经运行完毕了 final_suspend, 或者已经被 coro.destroy() 给析构了, 那么协程的储存空间已经被释放了. 再次对 coro 做任何的操作都会导致 seg fault.

6.co_await与await_transform

co_await cast-expression

co_await 表达式只能出现在协程里. 协程就有与之相伴的 promise .

如果这个 promise 提供了 await_transform(cast-expression) 的方法, 那么就会使用它, 变成
co_await promise.await_transform(cast-expression)
之后会看右边的 promise.await_transform(cast-expression) 或者 cast-expression 这个表达式有没有提供 operator co_await , 有的话就用上. 如果都没有的话就不转换类型了.

这个 co_await 表达式的类型和 await_resume 的返回类型一样.
如果这个 await_suspend(h) 抛出了异常, 那么协程立即 catch 异常, 恢复运行, 然后再次抛出异常.

7.coroutine_traits

coroutine_traits用于在编译阶段用来查找和检测满足特定coroutine_name的promise_type。
也就是说,一个coroutine_name类型的promise_type并不是一定要定义在coroutine_name内部。

定义的协程函数:

R f()
{
    co_yield 1;
}

可能的实现:

template
struct coroutine_traits {};
 
template
requires requires { typename R::promise_type; }
struct coroutine_traits {
  using promise_type = typename R::promise_type;
};

编译器发现有coroutine函数时,会根据R,args查找struct std::coroutine_traits,如果没有,则产生一个(如上,使用R内部的promise_type定义),如果已被定义,就用已被定义的。然后检查struct std::coroutine_traits::promise_type是否满足如下描述的coroutine接口:

coroutine接口要求具有public promise_type结构,而promise_type中必须有get_return_object,initial_suspend,final_suspend,unhandled_exception四个函数;
另外,如有co_return调用,则promise_type结构中需要return_void/return_value函数;
有co_yield调用时,需要yield_value函数;
有co_await调用时,需要重载操作符co_await await_transform(expr)或expr,其中await_transform是promise_type结构中一个可选的函数。

如果成功的话,就可以认为该函数是正常的协程函数了。
实际的情况也许比较复杂,因为f()可能是静态函数,非静态函数或右值的非静态函数,对此,cppreference是这样描述的:

(1)静态函数时,promise_type的定义为std::coroutine_traits::promise_type
如task foo(std::string x, bool flag) 对应 std::coroutine_traits::promise_type
(2)非静态函数时,promise_type的定义为std::coroutine_traits::promise_type
如task my_class::method1(int x) const 对应 std::coroutine_traits::promise_type
(3)右值非静态函数时,promise_type的定义为std::coroutine_traits::promise_type
如task my_class::method1(int x) && 对应 std::coroutine_traits::promise_type

参考:
https://zhuanlan.zhihu.com/p/356752742
https://zhuanlan.zhihu.com/p/239792492
拓展阅读:https://lewissbaker.github.io/

你可能感兴趣的:(C/C++)