首页 > 其他分享 >协程介绍

协程介绍

时间:2023-10-22 11:04:55浏览次数:43  
标签:suspend handle await 介绍 return promise 协程

协程是能暂停执行以再之后恢复的函数,C++协程是无栈的:它们通过返回到调用方暂停执行,并且恢复执行所需的数据与栈分离存储,这样就可以编写异步执行的顺序代码【1】;

但使用起来还是需要一些学习成本,本文主要对C++协程的使用进行总结。

C++20中协程

C++20中提供了协程的支持,一个函数中包含co_await、co_yield、co_return关键字则都是协程,从一个简单的协程例子【2】入手分析协程的工作流程:

struct ReturnObject {
    struct promise_type {
        ReturnObject get_return_object() { return {}; }
        std::suspend_never initial_suspend() { return {}; }
        std::suspend_never final_suspend() noexcept { return {}; }
        void unhandled_exception() {}
    };  
};

struct Awaiter {
  std::coroutine_handle<> *hp_;
  constexpr bool await_ready() const noexcept { return false; }
  void await_suspend(std::coroutine_handle<> h) { *hp_ = h; }
  constexpr void await_resume() const noexcept {}
};

ReturnObject counter(std::coroutine_handle<> *continuation_out)
{
    Awaiter a{continuation_out};
    for (int32_t i = 0;; ++i) {
        co_await a;
        std::cout << "counter: " << i << std::endl;
    }   
}

int main()
{
    std::coroutine_handle<> h;
    counter(&h);
    for (int i = 0; i < 3; ++i) {
        std::cout << "In main function\n";
        h();
    }   
    h.destroy();
    return 0;
}
In main function
counter: 0
In main function
counter: 1
In main function
counter: 2

从输出上看counter函数通过协程挂起与恢复了三次;

代码中使用co_await关键字,同时又看到了很多额外的类与定义,包括ReturnObject、promise_type、Awaiter等,但这些之间是什么关系?

std::coroutine_handle

协程句柄封装了协程帧指针,提供了协程恢复、销毁以及与获取协程的promise_type类型对象的接口;

std::coroutine_handle 是一个模板类,查看其泛化版本提供的接口

_EXPORT_STD template <class _Promise>
struct coroutine_handle {
    constexpr coroutine_handle() noexcept = default;
    constexpr coroutine_handle(nullptr_t) noexcept {}
    // 通过promise对象构造一个协程句柄
    _NODISCARD static coroutine_handle from_promise(_Promise& _Prom) noexcept;
    coroutine_handle& operator=(nullptr_t) noexcept;
    _NODISCARD constexpr void* address() const noexcept;
    _NODISCARD static constexpr coroutine_handle from_address(void* const _Addr) noexcept;
    // 获取类型擦除句柄,即用户不关心promise对象
    constexpr operator coroutine_handle<>();
    // 判断协程是否结束
    constexpr explicit operator bool();
    // 判断协程是否结束
    _NODISCARD bool done() const noexcept;
    // 恢复协程
    void operator()() const; 
    // 恢复协程
    void resume() const;
    // 销毁协程
    void destroy() const noexcept;
    // 获取promise对象的引用
    _NODISCARD _Promise& promise() const noexcept;
};

coroutine_handle和promise对象可以相互转换,from_promise和promise接口;

std::coroutine_handle<promise_type> handle;
handle_.promise() // 通过handle访问promise,promise对象的引用
std::coroutine_handle<promise_type>::from_promise(*this) // 通过promise获取handle

例如修改上述例子,通过promise得到handle存在ReturnObject中:

struct ReturnObject {
  struct promise_type {
    ReturnObject get_return_object() {
      return {
        // Uses C++20 designated initializer syntax
        .h_ = std::coroutine_handle<promise_type>::from_promise(*this)
      };
    }
    std::suspend_never initial_suspend() { return {}; }
    std::suspend_never final_suspend() noexcept { return {}; }
    void unhandled_exception() {}
  };

  std::coroutine_handle<promise_type> h_;
  operator std::coroutine_handle<promise_type>() const { return h_; }
  // A coroutine_handle<promise_type> converts to coroutine_handle<>
  operator std::coroutine_handle<>() const { return h_; }
};

promise_type

C++异步编程中介绍了future和promise,通过promise可以获取到一个future,promise存储异步计算的值,通过future可以获取到;

在上述例子中,编译期会检查协程用户自定义返回类型ReturnObject中是否定了promise_type

template <typename _Result, typename = void> 
   struct __coroutine_traits_impl {};

  template <typename _Result>
    struct __coroutine__impl<_Result,
                    __void_t<typename _Result::promise_type>>
    {
      using promise_type = typename _Result::promise_type;
    };

  template <typename _Result, typename...>
    struct coroutine_traits : __coroutine_traits_impl<_Result> {};

同时编译期会检查是否存在get_return_object返回对应ReturnObject,ReturnObject即为future/promise中的future,promise即为上述例子中的promise_type;

在运行期通过构造promise_type对象,通过get_return_object创建ReturnObject对象;

编译期代码变换

在编译期编译器会对协程进行代码变换,上述例子中代码转换后为:

ReturnObject counter(std::coroutine_handle<> *continuation_out)
{
    ... // 构造协程栈帧,申请内存,实参拷贝,promise_type、ReturnObject构造
    // ReturnType::promise_type构造promise_type
    // 构造ReturnObject,promise.get_return_object();

    co_await promise.initial_suspend();
    try {
        Awaiter a{continuation_out};
        for (int32_t i = 0;; ++i) {
            co_await a;  // 进一步转换
            std::cout << "counter: " << i << std::endl;
        } 
    } catch (...) {
        promise.unhandled_exception();
    }
    ...// 释放协程局部变量
    // 若没有co_return,则会协程体结束隐式插入co_return
    final-suspend:
        co_await promise.final-suspend();
}

代码中的co_await a => co_await <expr> 变换为:

{
  auto&& value = <expr>; // expr 执行返回awaitable 对象
  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> // 在await_suspend之前就将协程的状态标记为挂起

    if constexpr (std::is_void_v<await_suspend_result_t>)
    {
      awaiter.await_suspend(handle_t::from_promise(p)); // 将promise对象返回给
      <return-to-caller-or-resumer>
    }
    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)))  // 返回为false,则恢复当前协程
      {
        <return-to-caller-or-resumer>
      }
    }

    <resume-point> // 协程在 co_await 表达式中暂停而在后来恢复,那么恢复点处于紧接对 awaiter.await_resume() 的调用之前
  }

  return awaiter.await_resume();
}

其中get_awaitable和get_awaiter为:

template<typename P, typename T>
decltype(auto) get_awaitable(P& promise, T&& expr)
{
  if constexpr (has_any_await_transform_member_v<P>)
    return promise.await_transform(static_cast<T&&>(expr));
  else
    return static_cast<T&&>(expr);
}

template<typename Awaitable>
decltype(auto) get_awaiter(Awaitable&& awaitable)
{
  if constexpr (has_member_operator_co_await_v<Awaitable>)
    return static_cast<Awaitable&&>(awaitable).operator co_await();
  else if constexpr (has_non_member_operator_co_await_v<Awaitable&&>)
    return operator co_await(static_cast<Awaitable&&>(awaitable));
  else
    return static_cast<Awaitable&&>(awaitable);
}

在变换后的代码中可以看到,promise对象构造之后通过get_return_object获取ReturnObject,也说明了为什么promise_type中需要initial_suspend、final_suspend、unhandled_exception等函数。

对于协程体中的实参要注意:在协程帧分配内存后,将调用者传递给协程的实参拷贝到协程帧中,以便后续协程挂起后仍能访问这些实参,如果是引用传递则无法保证调用者的临时变量的生命周期,导致协程中访问引用出现非法内存访问;【3】

co_await

co_await <exp> 表达式需要返回一个awaiter对象,在转换后的代码中,表达式先进行执行,返回一个awaitable对象,首先检查promise对象中是否存在await_transform方法,获取awaitable对象;再检查awaitable对象是否重载了co_await操作符并返回最终的awaiter对象【3】,上述例子中自定义了Awaiter类型。

在执行协程代码之前有:

co_await promise.initial_suspend();

initial_suspend接口允许在执行协程的代码之前判断是否需要挂起,返回的awaitable对象标准库中提供了两种默认实现:

struct suspend_always
  {
    constexpr bool await_ready() const noexcept { return false; } // 挂起
    constexpr void await_suspend(coroutine_handle<>) const noexcept {}
    constexpr void await_resume() const noexcept {}
  };  

  struct suspend_never
  {
    constexpr bool await_ready() const noexcept { return true; } // 不挂起
    constexpr void await_suspend(coroutine_handle<>) const noexcept {}
    constexpr void await_resume() const noexcept {}
  };

而co_await <exp>整个表达式的返回值最终为awaiter.await_resume();

在co_await转换后的代码中,调用 awaiter.await_ready()返回 false,那么暂停协程;

调用 awaiter.await_suspend(handle),其中handle是表示当前协程的协程句柄,可以通过这个句柄观察暂停的协程,而且此函数负责调度它以在某个执行器上恢复,或将其销毁【1】; 如果 await_suspend 返回 void,那么立即将控制返回给当前协程的调用方/恢复方(此协程保持暂停),否则 如果 await_suspend 返回 bool,那么: 值为 true 时将控制返回给当前协程的调用方/恢复方 值为 false 时恢复当前协程。 如果 await_suspend 返回某个其他协程的协程句柄,那么(通过调用 handle.resume())恢复该句柄;

在【4】中介绍了await_suspend 其他协程句柄的设计避免了栈溢出问题

结合编译期转换后的代码,总结协程流程如下:

协程介绍_cpp

分析第一节中的示例代码:

step1: 调用counter,构造promise对象与协程相关状态参数,注意promise_type要在Task中定义

step2: 由于init_suspend返回std::suspend_never,因此不会挂起

step3: 运行到co_await a

step4: Awaiter 中await_ready返回false,因此挂起协程,将ReturnObject返回给调用者

step5: 进行main函数中的循环,调用handle的resume,恢复协程

step6: 恢复协程状态,调用对应Awaiter对象的await_resume方法,重新调用到step2

由于协程体for循环没有结束条件,因此不会执行到promise的final_suspend方法,直到main函数中执行handle的destroy,promise对象生命周期结束,销毁协程相关参数

co_yield

co_yield <expr> 等价与 co_await promise.yield_value(expr);

协程介绍_cpp_02

在理解了第一节中的代码执行流程后,后面就比较好理解了,promise_type中需要增加yield_value方法,入参类型需要和<expr>返回的类型一致,这次重新修改一个代码例子进行分析:

struct ReturnObject {
    struct promise_type {
        struct FinalAwaiter {
            bool await_ready() const noexcept { return true; }
            void await_suspend(std::coroutine_handle<promise_type> h) noexcept { }
            void await_resume() noexcept {
                DBG_LOG();
            }
        };

        struct Awaiter {
            ~Awaiter()
            {
                DBG_LOG("~Awaiter");
            }
            bool await_ready() const noexcept { return false; }
            void await_suspend(std::coroutine_handle<promise_type> h) noexcept {
                DBG_LOG();
            }
            void await_resume() const noexcept {}
        };

        promise_type()
        {
            DBG_LOG("%p", this);
        }
        ~promise_type()
        {
            DBG_LOG("%p", this);
        }

        ReturnObject get_return_object() {
            return {.h_ = std::coroutine_handle<promise_type>::from_promise(*this)};
        }
        std::suspend_never initial_suspend()
        {
            DBG_LOG();
            return {};
        }
        FinalAwaiter final_suspend() noexcept
        {
            DBG_LOG();
            return {};
        }
        Awaiter yield_value(unsigned value) {
            DBG_LOG();
            value_ = value;
            return {};
        }
        void return_void()
        {
            DBG_LOG();
        }
        void unhandled_exception() {}

        uint32_t value_;
    };
    std::coroutine_handle<promise_type> h_;
};

ReturnObject counter()
{
    co_yield 10;
}

int main()
{
    auto r = counter();
    r.h_();
    return 0;
}

step1: 调用counter,构造promise对象与协程相关状态参数

step2: 由于init_suspend返回std::suspend_never,因此不会挂起

step3: 运行到co_yield 10,即调用co_await promise.yield_value(10)

step4: 调用promise_type::yield_value,返回Awaiter

step5: Awaiter 中await_ready返回false,因此挂起协程,将ReturnObject返回给调用者

step6: 回到main函数,拿到ReturnObject,resume协程

step7: 恢复协程状态,调用对应Awaiter对象的await_resume方法;

Awaiter对象开始析构

step8: 协程状态转换为done,析构协程函数中的局部变量;隐式插入co_return,调用promise.return_void()

step9: 调用到co_await promise.final_suspend(),在此之前协程状态转换为done

step10: 由于FinalAwaiter::await_ready返回true,则协程不再挂起,运行到FinalAwaiter::await_resume

step11: promise_type析构,销毁协程实参,协程自动销毁后续不能再通过handle进行destroy,否则会出现双重释放

这里修改FinalAwaiter::await_ready返回false,则不会再调用到FinalAwaiter::await_resume,协程挂起;

而此时step9的时候协程状态已经为done,不能再调用handle resume,这个时候协程交由调用者进行手动销毁,通常将ReturnObject修改为RAII类,在其析构函数中自动调用handle destroy;

~ReturnObject() {
        DBG_LOG();
        h_.destroy();
    }

对于协程实参拷贝,可以看测试代码:

协程介绍_cpp_03

如果counter中送入是Test引用,协程将传递引用给协程,在前文“编译期代码变换”中已有说明会可能导致非法内存访问;

co_return

协程介绍_cpp_04

co_return 相当于调用了 promise.return_value() 或者 promise.return_void() 然后跳到 final-suspend 标签那里, 也就是说这个这个协程结束了, 再也无法被恢复了。在进入final_suspend之前,会逆序析构协程体内的局部变量,但这时候不会析构协程的实参【3】;

流程与上面分析类似,在此不再赘述;

限制

协程不能使用变长实参,普通的 return 语句,或占位符返回类型(auto 或 Concept)。

constexpr 函数、构造函数、析构函数及 main 函数 不能是协程【1】。

参考资料

【1】https://en.cppreference.com/w/cpp/language/coroutines

【2】 My tutorial and take on C++20 coroutines (stanford.edu)

【3】C++20高级编程

【4】https://lewissbaker.github.io/2020/05/11/understanding_symmetric_transfer

标签:suspend,handle,await,介绍,return,promise,协程
From: https://blog.51cto.com/u_13137973/7974396

相关文章

  • 芯片项目介绍-02
    目录简介数字芯片会用到两种工艺,一种是Flashprocess工艺,芯片内部可以跑程序,没有程序,芯片就是一个砖头,程序存储与芯片内部的Flash中;另外一种就是LogicProcess,逻辑工艺,不带Flash存储体,将程序放在NorFlash或者NandFlash中,总之是放在芯片外部的存储体中,通过芯片中......
  • 芯片项目介绍-01
    Linux基本操作及Gvim基本操作通常使用Linux系统进行设计#创建文件夹mkdirmyprojmkdirdesign_labscp-rf文件路径复制到的路径chmod777文件芯片产品流程综述代码设计-迭代过程综合之后会进行formalcheck,形式验证综合之后会进行初步的STAAPR之后......
  • 循序渐进介绍基于CommunityToolkit.Mvvm 和HandyControl的WPF应用端开发(11) -- 下拉
    在我们开发的前端项目中,往往为了方便,都需对一些控件进行自定义的处理,以便实现快速的数据绑定以及便捷的使用,本篇随笔介绍通过抽取常见字典列表,实现通用的字典类型绑定;以及通过自定义控件的属性处理,实现系统字典内容的快捷绑定的操作。1、下拉列表的数据绑定在我们创建下拉列表的......
  • 数据库介绍
    数据库(Database)是按照数据结构来组织、存储和管理数据的仓库,它产生于距今六十多年前,随着信息技术和市场的发展,特别是二十世纪九十年代以后,数据管理不再仅仅是存储和管理数据,而转变成用户所需要的各种数据管理的方式。数据库有很多种类型,从最简单的存储有各种数据的表格到能够进......
  • SQL介绍
    结构化查询语言是高级的非过程化编程语言,允许用户在高层数据结构上工作。它不要求用户指定对数据的存放方法,也不需要用户了解具体的数据存放方式,所以具有完全不同底层结构的不同数据库系统,可以使用相同的结构化查询语言作为数据输入与管理的接口。结构化查询语言语句可以嵌套,这......
  • linux磁盘管理-RAID介绍
    一、RAID介绍RAID(RedundantArrayofIndependentDisk独立冗余磁盘阵列)技术是加州大学伯克利分校1987年提出,最初是为了组合小的廉价磁盘来代替大的昂贵磁盘,同时希望磁盘失效时不会使对数据的访问受损失而开发出一定水平的数据保护技术。RAID就是一种由多块廉价磁盘构成的冗余......
  • postgis常用函数介绍(二)
    概述:书接上文,本文继续讲解Postgres中常用的空间函数的使用。 常用函数:1、判断geometry是否为空通过函数st_isempty(geom)可以判断geometry是否为空,返回是布尔型的true或者false,具体使用如下:  2、判断一个geometry是否在一个geometry里面通过函数st_within(geom,geom......
  • day11-JNI介绍
    四JNI介绍和安装4.1JNI介绍JNI,javanativeinterface,Java本地开发接口,实现在安卓中JAVA和C语言之间的相互调用。#之前写安卓,全是用java写#后期可以用c写安卓,写了后,需要使用java调用c的方法,完成功能4.2NDK安装NDK是JNI开发的工具包NDK,NativeDevelopKits,本地开发工......
  • kafka介绍
    Kafka名字的由来kafka的架构师jaykreps对于kafka的名称由来是这样讲的,由于jaykreps非常喜欢franzkafka,并且觉得kafka这个名字很酷,因此取了个和消息传递系统完全不相干的名称kafka,该名字并没有特别的含义。Kafka的诞生kafka的诞生,是为了解决linkedin的数据管道问题,起初linkedin......
  • umich cv-4-1 卷积网络基本组成部分介绍
    这节课中介绍了卷积网络的基本组成部分(全连接层,激活函数,卷积层,池化层,标准化等),下节课讨论了卷积神经网络的发展历史以及几种经典结构是如何构建的卷积网络组成部分前言卷积层池化层normalization前言在之前提到的全连接神经网络中,我们直接把一个比如说32*32*3的......