C++20: Coroutine学习笔记

C++20: Coroutine

Part 0. 引言

C++20引入了新的语言特性:协程(Coroutine),为C++的异步程序编写提供了新的范式,C++20对协程的设计提供了大量的可定制点,提高了协程程序的灵活度,但也加大了对协程程序的理解难度,本篇文章旨在带领读者了解协程的基本使用、相关概念、<coroutine>头文件的介绍。

本文中所有程序的运行结果均是在:Windows, mingw64-posix-seh-gcc10.2.0的环境下产生的

Part 1. 协程的执行

为了了解协程是如何执行的,我们从一个简单的协程程序入手:

#include <iostream>
#include <coroutine>

struct Awaitable {
    bool await_ready() {
        std::cout << "is not ready" << std::endl;
        return false;
    }

    void await_suspend(std::coroutine_handle<>) {
        std::cout << "suspend" << std::endl;
    }

    void await_resume() {
        std::cout << "resumed" << std::endl;
    }
};

struct Promise {
    struct promise_type {
        Promise get_return_object() {
            std::cout << "get return object" << std::endl;
            return Promise{ std::coroutine_handle<promise_type>::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;
        }
    };

    std::coroutine_handle<promise_type> coro_handle;
};

Promise foo() {
    co_await Awaiter{};
}

int main() {
    auto p = foo();
    p.coro_handle.resume();
    return 0;
}

这个程序的输出为:

get return object
initial suspend
is not ready
suspend
resumed
return void
final suspend

上面这个程序有很多令人疑惑的点:比如为什么foo()函数没有return语句,却仍然有返回值;co_await表达式的含义;promise_type的函数为什么被调用了……等等,下面我们一一来解释。

我们需要知道的是,协程是一个函数,这个函数的返回值类型的定义内需要含有符合要求的类型promise_type;而且,这个函数内部至少含有co_awaitco_yield或者co_return语句,且不能含有普通的return语句;在协程被调用(或者使用co_*语句)时,编译器会按照一定规则展开协程调用处的代码。

就上面的输出结果,这个协程是这样执行的:

首先,当一个协程开始执行时会调用operator new来创建一个coroutine state

什么是coroutine state

coroutine state包含了:

1. 一个promise_type对象
2. 按值复制的协程参数
3. 挂起点的某种表示
4. 生存期跨过当前暂停点的局部变量和临时量

然后,会构建promise_type对象

其次,调用promise_type::get_return_object作为返回值赋值给p

紧接着,调用co_await promise_type::initial_suspend()来决定是否立即执行协程

co_await promise_type::inital_suspend()中发生了什么(下文同理)

首先,会调用promise_type::initial_suspend().await_ready()来判断是否挂起协程

如果返回true,则不挂起协程,调用promise_type::initial_suspend().await_resume();并且await_resume()的返回值作为co_await expr表达式的返回值

如果返回false,调用promsise_type::initial_suspend().await_suspend;如果返回值为void/true,挂起协程;如果返回值为false,则调用promise_type::initial_suspend().await_resume;如果返回值为另一个coroutine_handle,则切换到另一个协程执行。

上面的程序中,inital_suspend()返回的是std::suspend_never,那么协程不会被挂起,开始执行协程。

执行到co_await Awaitable{};这一行,由于Awaitable::await_ready()的返回值为false,因此协程被挂起,调用void Awaitable::await_suspend(),将控制权交给协程的调用方,也就是main函数

main函数内通过p获取了协程的句柄,调用p.coro_handle.resume()来唤起协程,调用Awaitable::resume(),协程继续执行,并且co_await expr的表达式求值完毕

协程内无co_return语句(同无操作数的co_return一样),调用promise_type::return_void()

调用co_await promise_type::final_suspend(),协程结束,由于promise_type::final_suspend()返回std::suspend_never,协程不挂起,立即销毁coroutine state对象,调用std::suspend_never::await_resume()

说到这里,对协程的执行,我们应该有了一个大致的了解:promise_type是连接协程内部与协程调用者的桥梁,而awaitable决定了协程挂起和唤醒时的行为,它们内部的函数按照一定的的规则被调用,而coroutine_handle是外部操纵和控制协程的对象。

用流程图来表示:

coroutine process 1

coroutine process 2

图源:知乎文章用户@linux


对于一个协程的调用,编译器展开的伪代码大致如下
{
  co_await promise.initial_suspend();
  try
  {
    coroutine body;
  }
  catch (...)
  {
    promise.unhandled_exception();
  }
FinalSuspend:
  co_await promise.final_suspend();
}
对于co_await expr表达式,编译器展开的伪代码大致如下
{
  auto&& value = <expr>;
  auto&& awaitable = get_awaitable(promise, static_cast<decltype(value)>(value));
  auto&& awaiter = get_awaiter(static_cast<decltype(awaitable)>(awaitable));
  if (!awaiter.await_ready()) //是否需要挂起协程
  {
    using handle_t = std::experimental::coroutine_handle<P>;
    using await_suspend_result_t =
      decltype(awaiter.await_suspend(handle_t::from_promise(p)));
    <suspend-coroutine> //挂起协程
    if constexpr (std::is_void_v<await_suspend_result_t>)
    {
      awaiter.await_suspend(handle_t::from_promise(p)); //异步(也可能同步)执行task
      <return-to-caller-or-resumer> //返回给caller
    }
    else
    {
      static_assert(
         std::is_same_v<await_suspend_result_t, bool>,
         "await_suspend() must return 'void' or 'bool'.");
      if (awaiter.await_suspend(handle_t::from_promise(p)))
      {
        <return-to-caller-or-resumer>
      }
    }
    <resume-point> //task执行完成,恢复协程,这里是协程恢复执行的地方
  }
  return awaiter.await_resume(); //返回task结果
}

Part 1+. 协程句柄,协程关键字的使用

std::coroutine_handle<P>

std::coroutine_handle<P>是我们作为协程的调用者,用于操控协程的类型。

对于std::coroutine_handle<P>,其模板参数P类型必须满足约束Promise,简单地来说,类型P的定义内需要含有promise_type类型。

std::coroutine_handle<P>可以用于恢复协程的执行,判断协程是否执行完毕,访问协程的承诺对象,以及显式地销毁协程。

  • 调用std::coroutine_handle<P>::operator ()或者std::coroutine_handle<P>::resume()可以唤起协程
  • 调用std::coroutine_handle<P>::done(),通过返回的bool值可以判断协程是否执行完毕
  • 调用std::coroutine_handle<P>::promise(),可以(以引用形式Promise&)获取协程的承诺对象
  • 调用std::coroutine_handle<P>::destory(),可以销毁协程

特别地,它的静态成员函数from_address能够从promise_type类型的对象创建一个coroutine_handle<promise_type>对象。

co_awaitco_yieldco_return

本质上来讲,这三个关键字都是编译器提供的语法糖,co_await的展开形式上文中已经提到,下面主要说明后两者的展开形式

对于co_yield expr 而言,它等价于co_await promise.yield_value(expr)

对于co_return而言,当程序执行到co_return所在位置,如果表达式为co_return;,则调用promise.return_void()
否则,如果是co_return expr,则调用promise.return_value(expr),随后按照协程的正常程序终止。

Part 2. 协程相关类型的实现要求

Awaitable的实现要求

一个能被协程使用的Awaitable类型应该含有成员:

  • bool await_ready(),该函数决定了co_await awaiter时,是否挂起协程(返回true继续执行,返回false挂起)
  • await_suspend(std::coroutine_handle<P>),该函数决定了挂起协程时的行为,并且
    • 如果返回类型为void_t,或者返回类型为bool且返回值为true,则挂起协程,控制权转交给协程调用方
    • 如果返回类型为bool且返回值为false,则取消挂起协程
    • 如果返回类型为coroutine_handle<P>,则挂起协程,控制权转交给coroutine_handle<P>指定的协程
  • await_resume(),该函数决定了唤起协程时的行为,且
    • 如果返回类型为void,则co_await awaiter表达式无返回值
    • 如果返回类型为T,则co_await awaiter表达式的返回值为T类型的对象

promise_type的实现要求

一个能被协程使用的promise_type应该至少含有成员:

  • R get_return_object(),该函数决定了协程调用处获得的返回值
  • awaitable initial_suspend(),该函数决定了协程启动时的行为
  • awaitable final_suspend(),该函数决定了协程结束,或者协程出现异常被迫停止后的行为
  • void unhandled_exception() noexcept,该函数决定了协程执行过程中出现未捕获的异常后的行为
  • void return_void()或者void return_value(T),用于决定co_return expr的行为。这两个函数在同一promise_type中只能存在一个

另外地,还可以添加以下成员用以扩展协程功能:

  • std::suspend_always yield_value(T),用于决定co_yield expr的行为。需要注意的是,co_yield只是一种语法糖形式,本质上是调用了yield_value(T),如果该函数返回的awaiter没有指示协程挂起,那么与co_yield expr的预期语义相悖。
  • R get_return_object_on_allocation_failure(),该函数用于在coroutine state分配失败时调用,并且作为协程调用处的返回值
  • A await_transform(T),该函数用于co_await expr中,将expr的求值结果转换为特定的awaitable类型。需要注意的是,如果await_transform(expr)的求值结果类型定义了operator co_await(T),那么优先调用await_transform,以获得awaitable类型,然后调用operator co_await以获得awaiter,否则,以awaitable本身作为awaiter

Part 3. 协程的异常处理(等待更新)

Part 4. 有关协程的细节和注意事项(等待更新)

0x00000001: 协程创建、销毁时,构造、析构函数的调用

Part 5. 协程的完整使用示例(等待更新)

参考、引用及相关文章

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇
隐藏
变装