C++20 新特性 协程 Coroutines(2)

 更新时间:2021年10月08日 10:45:03   作者:孙孟越  
上篇文章简单给大介绍了 C++20 特性 协程 Coroutines co_yield 和 co_return 那么这篇文章继续给大家介绍C++20 的新特性协程 Coroutines co_await,需要的朋友可以参考一下

想了解上一篇文章内容的小伙伴可点击 C++20 特性 协程 Coroutines (1)

谈到什么是协程. 并且介绍了 co_yield co_return 的作用. 这篇来介绍一下 co_await.

1、co_await

一个形如:

co_await awaitable

的表达式就叫一个 await-expression. co_await 表达式是用来暂停当前协程的运行, 转而等待 awaitable 的结果. 然后 awaitable 进行计算, 最终返回一个 awaiter 结构用来告诉 co_await 要做什么.

co_await 所在的函数块本身就是协程, 所以这个 co_await 也得配上一个 promise 和一个 coroutine_handle. 就像上篇文章里面 generator 类之类的东西.

这个 awaitable 可以是很多东西, 首先会检查 promise 有没有提供 await_transform 函数, 如果有就会用上, 没有就不管.

(只要提供了任何一个 await_transform, 那么每一个 awaitable 都需要找到适合它的重载, 否则就会报错. 库的实现者可以通过 await_transform 接口来限制哪些 awaitable 可以用在协程之中. 参见https://stackoverflow.com/q/65787797/14406396 )

之后的话, 会查找 operator co_await 这个函数, 预期这个 operator 返回一个 awaiter.已经是一个 awaiter 了.

2、awaiter 的三个接口用途

一个 awaiter 需要实现三个接口 await_ready() , await_suspend(std::coroutine_handle<P>) , await_resume() .

只要实现了这三个接口的东西就是 awaiter.

await_ready() 告诉 co_await 自己好了没.

await_suspend(h) 可以选择返回 void , bool , std::coroutine_handle<P> 之一. h 是本协程的 handle. P是本协程的 promise 类型 (或者是 void, 见第三篇中的解释).

如果 await_ready() 返回 false , 这个协程就会暂停. 之后:

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

如果 await_ready() 返回 true 或者协程被恢复了, 那么就执行 await_resume() , 它得到的结果就是最终结果.

所以说, 这await_ready, await_suspend, await_resume 三个接口分别表示 "有没有准备好", "停不停", "好了该咋办". 设计还是很自然的.

C++ 的协程是非对称协程, 是有一个调用/被调用的关系的. 一个协程被某个东西唤醒了, 那么它下次暂停的时候, 就会把控制流还给那个唤醒它的东西. 所以 C++ 的协程完全可以看作是一个可重入的函数.

3、协程用法的回顾

再来看上一篇文章中的伪代码

{
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(); // 最后一次暂停
}

catch 块里面出现的 !initial-await-resume-called 就是指 promise.initial_suspend() 返回的那个 await_resume() 有没有被执行过.

如果执行了, 那么这个 flag 就会立刻变成 true. 然后调用 promise.unhandled_exception() 来处理异常.

一个例子:

由于 co_await 对这三个东西的应该做什么没有做任何限制, 所以可以用来实现很多功能.

举个例子 (来自标准库), 比如我们想要设计一个协程, 能够停下任意的正时长, 就可以这样设计:

template <class Rep, class Period>
auto operator co_await(std::chrono::duration<Rep, Period> d) // operator co_await
{
    struct awaiter
    {
        std::chrono::system_clock::duration duration;
        awaiter(std::chrono::system_clock::duration d) : duration(d) {}
        bool await_ready() const { return duration.count() <= 0; }
        int await_resume() {  return 1;  }
        void await_suspend(std::coroutine_handle<> h)
        {
            std::this_thread::sleep_for(duration);
        }
    };
    return awaiter{d};
}


这样的话, 如果输入一个正的时间, 就会调用 await_suspend() 进行暂停了. 如果输入的时间是负的, 那就通过 await_ready() 返回 true 绕过了这个过程.

当然, 调用它需要在一个协程中, 也就意味着需要一个 promise coroutine_handle 包装类的配合. 像这样

struct my_future
{
    struct promise_type;
    using handle = std::coroutine_handle<promise_type>;
    struct promise_type
    {
        int current_value;
        auto initial_suspend() { return std::suspend_always{}; }
        auto final_suspend() { return std::suspend_always{}; }
        void unhandled_exception() { std::terminate(); }
        /* ... */
    };
    /* ... */
private:
    my_future(handle h) : coro(h) {}
    handle coro;
};

my_future sleep_coro()
{
    printf("Start sleeping\n");
    int ans = co_await 1s;
    printf("End sleeping, with ans = %d\n", ans);
}

当然, 一个函数也可以放在 co_await 的右边, 就像 co_await g(); 只要返回的结构里面有那三个 await_* 接口就行. 甚至你可以直接 co_await std::suspend_always{};

下面是协程流控的细致分析.

int main()
{
    auto h = sleep_coro(); 
// 这一步创建协程, 在 co_await initial_suspend 处, 执行完 await_ready, await_suspend. 返回 main
// 注意 initial_suspend 返回的是 std::suspend_always{}
// 所以是一定暂停, 并且 resume 的时候什么都不做

    h.resume();
// 这一步执行上一个 await_resume 以后(什么都不做), 执行了 printf("Start sleeping\n");
// 然后收到 co_await 1s 返回的结构, 其中 await_suspend 里面需要暂停.
// 然后执行完 await_ready, await_suspend (在这个函数里暂停 1s), 返回 main

    h.resume();
// 这一步执行完 await_resume 以后(初始化 ans = 1)
// 执行了 printf("End sleeping, with ans = %d\n", ans);
// 然后在 co_await final_suspend 处执行完 await_ready, await_suspend. 就返回 main

}

示列代码见这里

到这里大家可以重新会到(1)去看看:C++20 特性 协程 Coroutines(1)

到此这篇关于C++20 新特性 协程 Coroutines(2)的文章就介绍到这了,更多相关C++20 协程 Coroutines内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • C语言分支循环其嵌套语句的使用

    C语言分支循环其嵌套语句的使用

    本文主要介绍了switch 嵌套和循环嵌套,文中通过示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2021-11-11
  • C++ Template应用详解

    C++ Template应用详解

    本篇文章主要介绍了C++ Template应用详解,模板(Template)指C++程序设计设计语言中采用类型作为参数的程序设计,支持通用程序设计。
    2016-12-12
  • c++中cin实现输入字符串方式

    c++中cin实现输入字符串方式

    这篇文章主要介绍了c++中cin实现输入字符串方式,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-11-11
  • 浅析多维数组的下标重载

    浅析多维数组的下标重载

    贴一下实现基本功能的代码吧,像越界检测,及其他功能就没写了,只要体现了思路,其他的功能好加
    2013-09-09
  • C语言使用函数实现字符串部分复制问题

    C语言使用函数实现字符串部分复制问题

    这篇文章主要介绍了C语言使用函数实现字符串部分复制问题,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-11-11
  • C++ 流插入和流提取运算符的重载的实现

    C++ 流插入和流提取运算符的重载的实现

    这篇文章主要介绍了C++ 流插入和流提取运算符的重载的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2019-12-12
  • C语言版医院管理系统

    C语言版医院管理系统

    这篇文章主要为大家详细介绍了C语言版医院管理系统,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2019-01-01
  • Visual Studio Code 配置C、C++ 文件debug调试环境的详细过程

    Visual Studio Code 配置C、C++ 文件debug调试环境的详细过程

    这篇文章主要介绍了Visual Studio Code 配置C、C++ 文件debug调试环境,本文通过图文并茂的形式给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2022-02-02
  • 关于VS2019 C++项目同时出现LNK2005 和LNK1169 error 的解决办法

    关于VS2019 C++项目同时出现LNK2005 和LNK1169 error 的解决办法

    这篇文章主要介绍了关于VS2019 C++项目同时出现LNK2005 和LNK1169 error 的解决办法,本文给大家介绍的非常详细,对大家的学习工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2021-04-04
  • c++访问修饰符与继承关系详解

    c++访问修饰符与继承关系详解

    C++提供了三个修饰符来限定类成员的被访问权限,分别是public、protected、private,通过限定访问权限,可以达到程序编写者想要解决的安全问题和权限问题,本文给大家介绍c++访问修饰符与继承关系,感兴趣的朋友一起看看吧
    2023-10-10

最新评论