探究C++20协程(1)——C++协程概览

什么是协程?

协程就是一段可以挂起(suspend)和恢复(resume)的程序,一般而言,就是一个支持挂起和恢复的函数。

一般情况下,函数一旦开始,就无法暂停。如果一个函数能够暂停,那它就可以被认为是我们开头提到的协程。所以挂起(suspend)就可以理解成暂停,恢复(resume)就理解成从暂停的地方继续执行。

Result Coroutine() {
  std::cout << 1 << std::endl;
  co_await std::suspend_always{};
  std::cout << 2 << std::endl;
  co_await std::suspend_always{};
  std::cout << 3 << std::endl;
};

Result 是按照协程的规则定义的类型,之后再详细介绍。在 C++ 20当中,一个函数的返回值类型如果是符合协程的规则的类型,那么这个函数就是一个协程。

co_await std::suspend_always{};,其中 co_await 是个关键字,会使得当前函数(协程)的执行被挂起。

在控制台看到输出 1 以后,很可能过了很久才看到 2,这个“很久”也一般不是因为当前执行的线程被阻塞了,而是当前函数(协程)执行的位置被存起来,在将来某个时间点又读取出来继续执行的。

协程的状态

对应线程,协程也有执行,挂起,恢复,异常,返回状态。

C++ 协程会在开始执行时的第一步就使用 operator new 来开辟一块内存来存放这些信息,这块内存或者说这个对象又被称为协程的状态(coroutine state)。

协程的状态不仅会被用于存放挂起时的位置(后称为挂起点),也会在协程开始执行时存入协程体的参数值。例如:

Result Coroutine(int start_value) {
  std::cout << start_value << std::endl;
  co_await std::suspend_always{};
  std::cout << start_value + 1 << std::endl;
};

这里的 start_value 就会被存入协程的状态当中。

需要注意的是,如果参数是值类型,他们的值或被移动或被复制(取决于类型自身的复制构造和移动构造的定义)到协程的状态当中;如果是引用、指针类型,那么存入协程的状态的值将会是引用或指针本身,而不是其指向的对象,这时候需要开发者自行保证协程在挂起后续恢复执行时参数引用或者指针指向的对象仍然存活。

与创建相对应,在协程执行完成或者被外部主动销毁之后,协程的状态也随之被销毁释放(编译器处理,不需要显式调用)。

协程的挂起

C++ 通过 co_await 表达式来处理协程的挂起,表达式的操作对象则为等待体(awaiter)

等待体需要实现三个函数,这三个函数在挂起和恢复时分别调用。

await_ready

标准库当中提供了两个非常简单直接的等待体,struct suspend_always 表示总是挂起,struct suspend_never 表示总是不挂起。这二者的功能主要就是依赖 await_ready 函数的返回值:

struct suspend_never {
    constexpr bool await_ready() const noexcept {
        return true;  // 返回 true,总是不挂起
    }
    //等待体其他内容...
};

struct suspend_always {
    constexpr bool await_ready() const noexcept {
        return false; // 返回 false,总是挂起
    }
     //等待体其他内容...
};

await_suspend

await_ready 返回 false 时,协程就挂起了。这时候协程的局部变量和挂起点都会被存入协程的状态当中,await_suspend 被调用到。

返回值 await_suspend(std::coroutine_handle<> coroutine_handle);

参数 coroutine_handle 用来表示当前协程,可以在稍后合适的时机通过调用 resume 来恢复执行当前协程:

coroutine_handle.resume();

await_suspend 函数的返回值类型没有明确给出,因为它有以下几种选项:

  • 返回 void 类型或者返回 true,表示当前协程挂起之后将执行权还给当初调用或者恢复当前协程的函数。
  • 返回 false,则恢复执行当前协程。注意此时不同于 await_ready 返回 true 的情形,此时协程已经挂起,await_suspend 返回 false 相当于挂起又立即恢复。
  • 返回其他协程的 coroutine_handle 对象,这时候返回的 coroutine_handle 对应的协程被恢复执行。
  • 抛出异常,此时当前协程恢复执行,并在当前协程当中抛出异常。

await_resume

同样地,await_resume 的返回值类型也是不限定的,返回值将作为 co_await 表达式的返回值。

等待体示例

struct Awaiter {
  int value;

  bool await_ready() {
    // 协程挂起
    return false;
  }

  void await_suspend(std::coroutine_handle<> coroutine_handle) {
    // 切换线程
    std::async([=](){
      using namespace std::chrono_literals;
      // sleep 1s
      std::this_thread::sleep_for(1s); 
      // 恢复协程
      coroutine_handle.resume();
    });
  }

  int await_resume() {
    // value 将作为 co_await 表达式的值
    return value;
  }
};

await_ready:该方法用来检查协程是否需要挂起。如果返回 false,表示协程应该挂起,进入等待状态。

await_suspend:这个方法在 await_ready 返回 false 时被调用,用于挂起协程。

本样例的具体操作如下:

  • 使用 std::async 创建一个异步任务,该任务会在一个独立的线程上执行。
  • 在这个线程中,首先使线程暂停一秒钟(std::this_thread::sleep_for(1s))。
  • 暂停后,调用 coroutine_handle.resume() 以恢复被挂起的协程。

通过这种方式,await_suspend 方法实现了将协程暂停一段时间的效果,并且不阻塞协程所在的主线程。

await_resume:这个方法在协程恢复执行后被调用,用于传递协程的结果。在这个例子中,它返回结构体中的 value 成员变量,该变量将作为 co_await 表达式的结果。

协程的返回值类型

区别一个函数是不是协程,是通过它的返回值类型来判断的。如果它的返回值类型满足协程的规则,那这个函数就会被编译成协程。

规则就是返回值类型能够实例化下面的模板类型 _Coroutine_traits。

其再#include 文件中有定义如下

template <class _Ret, class = void>
struct _Coroutine_traits {};

template <class _Ret>
struct _Coroutine_traits<_Ret, void_t<typename _Ret::promise_type>> {
    using promise_type = typename _Ret::promise_type;
};

template <class _Ret, class...>
struct coroutine_traits : _Coroutine_traits<_Ret> {};

简单来说,就是返回值类型 _Ret 能够找到一个类型 _Ret::promise_type 与之相匹配。这个 promise_type 既可以是直接定义在 _Ret 当中的类型,也可以通过 using 指向已经存在的其他外部类型。

此时,可以给出 Result 的部分实现:

struct Result {
  struct promise_type {
    //内部实现 见下文
  };
};

协程返回值对象的构建

这时已经了解 C++ 当中如何界定一个协程。不过会产生一个新的问题,返回值是从哪儿来的?协程体当中并没有给出 Result 对象创建的代码。

实际上,Result 对象的创建是由 promise_type 负责的,需要定义一个 get_return_object 函数来处理对 Result 对象的创建:

struct Result {
  struct promise_type {

    Result get_return_object() {
      // 创建 Result 对象
      return {};
    }
    //其余函数
  };
};

不同于一般的函数,协程的返回值并不是在返回之前才创建,而是在协程的状态创建出来之后马上就创建的。也就是说,协程的状态被创建出来之后,会立即构造 promise_type 对象,进而调用 get_return_object 来创建返回值对象。

promise_type 类型的构造函数参数列表如果与协程的参数列表一致,那么构造 promise_type 时就会调用这个构造函数。否则,就通过默认无参构造函数来构造 promise_type。

协程体的执行

initial_suspend

协程体执行的第一步是调用 co_await promise.initial_suspend(),initial_suspend 的返回值就是一个等待对象(awaiter),如果返回值满足挂起的条件,则协程体在最一开始就立即挂起。这个点实际上非常重要,可以通过控制 initial_suspend 返回的等待体来实现协程的执行调度。

有关调度的内容见专栏后续。

协程体的返回情况(值,void,异常)

这个协程的返回并不是返回函数声明前面的类型,而是使用co_return。这个声明的Result更像是沟通协程和调用者的一个桥梁。

接下来执行协程体。协程体当中会存在 co_await、co_yield、co_return 三种协程特有的调用,其中

  • co_await 前面已经介绍过,用来将协程挂起。
  • co_yield 则是 co_await 的另一种实现,用于传值给协程的调用者或恢复者或被恢复者
  • co_return 则用来返回一个值或者从协程体返回。

对于返回一个值的情况,需要在 promise_type 当中定义一个函数

void return_value();//必须返回void

那么再调用co_return 时

co_return 1000;

1000 会作为参数传入,即 return_value 函数的参数 value 的值为 1000。这个值可以存到 promise_type 对象当中,外部的调用者可以获取到。

除了返回值的情况以外,C++ 协程当然也支持返回 void。只不过 promise_type 要定义的函数就不再是 return_value 了,而是 return_void 了:

struct Result {
  struct promise_type {
    
    void return_void() {
      ...
    }

    ...
  };
};

此时,协程内部就可以通过 co_return 来退出协程体了:

协程体除了正常返回以外,也可以抛出异常。异常实际上也是一种结果的类型,因此处理方式也与返回结果相似。我们只需要在 promise_type 当中定义一个函数,在异常抛出时这个函数就会被调用到:

struct Result {
  struct promise_type {
    
    void unhandled_exception() {
      exception_ = std::current_exception(); // 获取当前异常
    }

    ...
  };
};

final_suspend

当协程执行完成或者抛出异常之后会先清理局部变量,接着调用 final_suspend 来方便开发者自行处理其他资源的销毁逻辑。final_suspend 也可以返回一个等待体使得当前协程挂起,但之后当前协程应当通过 coroutine_handle 的 destroy 函数来直接销毁,而不是 resume。

一般来说final_suspend应该挂起协程,希望其销毁和result(接受了协程返回值)生命周期一致,避免提前销毁出现意外。

int main() {
    auto result=Coroutine(); 
    return 0;
} 

测试样例

#define __cpp_lib_coroutine

#include <iostream>
#include <coroutine>
#include <future>
#include <chrono>

using namespace std::chrono_literals;

void Fun() {
    std::cout << 1 << std::endl;
    std::cout << 2 << std::endl;
    std::cout << 3 << std::endl;
    std::cout << 4 << std::endl;
}

struct Result {
    struct promise_type {
        int value;
        std::suspend_never initial_suspend() {
            return {};
        }

        std::suspend_never final_suspend() noexcept {
            return {};
        }

        Result get_return_object() {
            return {};
        }

        void return_value(int _value) {
            value = _value;
            std::cout << "coroutine return " << value << '\n';
        }

        void unhandled_exception() {

        }
    };
};

struct Awaiter {
    int value;

    bool await_ready() {
        return false;
    }

    void await_suspend(std::coroutine_handle<> coroutine_handle) {
        std::async([=]() {
            std::this_thread::sleep_for(1s);
            coroutine_handle.resume();
            });
    }

    int await_resume() {
        return value;
    }
};


Result Coroutine() {
    std::cout << "Coroutine begin " << std::endl;
    Fun();
    std::cout << co_await Awaiter{.value = 1000} << std::endl;
    co_return 5;
};

int main() {
    auto result = Coroutine();
    std::this_thread::sleep_for(1s);
    //等待一下看协程返回结果
    return 0;
}

输出结果

Coroutine begin
1
2
3
4
1000
coroutine return 5

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/548503.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

用海外云手机高效率运营TikTok!

很多做国外社媒运营的公司&#xff0c;想要快速引流&#xff0c;往往一个账号是不够的&#xff0c;多数都是矩阵养号的方式&#xff0c;运营多个TikToK、Facebook、Instagram等账号&#xff0c;慢慢沉淀流量变现&#xff0c;而他们都在用海外云手机这款工具&#xff01; 海外云…

汽车零部件制造迎来智能化升级,3D视觉定位系统助力无人化生产线建设

随着新能源汽车市场的蓬勃发展&#xff0c;汽车零部件制造行业正面临着前所未有的机遇与挑战。为了提高产能和产品加工精度&#xff0c;某专业铝合金汽车零部件制造商决定引进智能生产线&#xff0c;其中&#xff0c;对成垛摆放的变速箱壳体进行机床上料成为关键一环。 传统的上…

CentOS7.9下载及安装教程

1. 下载CentOS7.9 CentOS用的最多的是7.6&#xff0c;7.9是7里面最新的&#xff0c;至于8以上的版本听说没有维护和更新了&#xff0c;这里以7.9为例。 下载&#xff1a;https://mirrors.aliyun.com/centos/7.9.2009/isos/x86_64/ 2. 新建虚拟机 新建虚拟机–>典型(推荐…

Clustering and Projected Clustering with Adaptive Neighbors 论文阅读

1 Abstract 许多聚类方法基于输入数据的相似性矩阵对数据组进行划分。因此&#xff0c;聚类结果高度依赖于数据相似性学习。由于相似性度量和数据聚类通常是分两步进行的&#xff0c;学习到的数据相似性可能不是数据聚类的最佳选择&#xff0c;从而导致次优结果。在本文中&…

LeetCode 面试经典150题 202.快乐数

题目&#xff1a; 编写一个算法来判断一个数 n 是不是快乐数。 「快乐数」 定义为&#xff1a; 对于一个正整数&#xff0c;每一次将该数替换为它每个位置上的数字的平方和。然后重复这个过程直到这个数变为 1&#xff0c;也可能是 无限循环 但始终变不到 1。如果这个过程 结…

Nacos源码分析,Nacos gRPC服务端通信渠道是如何启动的?

作为SpringCloudAlibaba微服务架构实战派上下册和RocketMQ消息中间件实战派上下册的作者胡弦&#xff0c;我来给大家带来Nacos源码分析的技术文章。 Nacos默认会启动两个gRPC服务端通信渠道&#xff0c;一个用于Nacos集群节点之间的交互&#xff08;GrpcClusterServer&#xf…

Python爬虫之实践(!福利!动态IP免费送!)

Python爬虫是一种强大的工具&#xff0c;它允许我们自动从互联网上收集数据。通过编写Python脚本&#xff0c;我们可以模拟浏览器的行为&#xff0c;发送HTTP请求&#xff0c;获取网页内容&#xff0c;并提取所需的数据。本文将指导你如何进行Python爬虫&#xff0c;包括准备环…

雨天充电桩使用攻略:雨中电动汽车充电必看!

随着电动车的普及&#xff0c;雨天使用充电桩已成为常态。 然而&#xff0c;在恶劣天气条件下充电需格外谨慎&#xff0c;否则可能会带来安全隐患。以下是使用充电桩的安全须知和操作技巧&#xff0c;让您在雨天充电时更加安心&#xff1a; 警惕水患风险&#xff1a;避免在积水…

Python高质量函数编写指南

The Ultimate Guide to Writing Functions 1.视频 https://www.youtube.com/watch?vyatgY4NpZXE 2.代码 https://github.com/ArjanCodes/2022-funcguide Python高质量函数编写指南 1. 一次做好一件事 from dataclasses import dataclass from datetime import datetimedatacl…

Python-VBA函数之旅-classmethod函数

目录 一、装饰器的定义&#xff1a; 二、装饰器类型&#xff1a; 三、装饰器的主要用途&#xff1a; 四、classmethod常用场景&#xff1a; 1、classmethod函数&#xff1a; 1-1、Python&#xff1a; 1-2、VBA&#xff1a; 2、相关文章&#xff1a; classmethod是 Pyth…

MySQL查询重复数据获取最新数据

方法一&#xff1a; 1055 - Expression #1 of SELECT list is not in GROUP BY clause and contains nonaggregated column ‘se_jck的博客-CSDN博客 这个错误是由于 MySQL 的新版本中默认开启了ONLY_FULL_GROUP_BY模式&#xff0c;即在 GROUP BY 语句中的 SELECT 列表中&…

数据湖技术选型——Flink+Paimon 方向

文章目录 前言Apache Iceberg存储索引metadataFormat V2小文件 Delta LakeApache Hudi存储索引COWMOR元数据表 Apache PaimonLSMTagconsumerChangelogPartial Update 前言 对比读写性能和对流批一体的支持情况&#xff0c;建议选择Apache Paimon截止2024年1月12日数据湖四大开…

【LAMMPS学习】八、基础知识(2.5)恒压器

8. 基础知识 此部分描述了如何使用 LAMMPS 为用户和开发人员执行各种任务。术语表页面还列出了 MD 术语&#xff0c;以及相应 LAMMPS 手册页的链接。 LAMMPS 源代码分发的 examples 目录中包含的示例输入脚本以及示例脚本页面上突出显示的示例输入脚本还展示了如何设置和运行各…

基于混合博弈的配电网与多综合能源微网优化运行

该文研究了同一配电网下的多个综合能源微网 (integrated energy microgrids&#xff0c;IEM)的协同管理问题&#xff0c;旨在通 过配电网运营商(distribution system operator&#xff0c;DSO)制定电能 价格以协调 IEM 联盟的机组调度、需求响应和成员间的点 对点(peer-to-peer…

使用Scrapy选择器提取豆瓣电影信息,并用正则表达式从介绍详情中获取指定信息

本文同步更新于博主个人博客&#xff1a;blog.buzzchat.top 一、Scrapy框架 1. 介绍 在当今数字化的时代&#xff0c;数据是一种宝贵的资源&#xff0c;而网络爬虫&#xff08;Web Scraping&#xff09;则是获取网络数据的重要工具之一。而在 Python 生态系统中&#xff0c;S…

Oracle和PG数据库临时表的差异,PG数据库如何删除临时表

现实的开发过程中使用 PG 数据库删除临时表发现如下报错&#xff0c;提示表 xxx 不存在&#xff1a; 问题原因&#xff1a; 调用删除语句&#xff0c;但是临时表不存在了。 解决方案&#xff1a; PG下用下面的方式来删除临时表或不进行删除&#xff08;会话级临时表会自动删除…

线性表的链式存储

文章目录 前言一、概念及特点二、链表术语及分类三、单链表1.特点2.C语言实现3.头结点作用4.基本操作的具体实现 总结 前言 T_T此专栏用于记录数据结构及算法的&#xff08;痛苦&#xff09;学习历程&#xff0c;便于日后复习&#xff08;这种事情不要啊&#xff09;。所用教材…

Cannot access ‘androidx.activity.FullyDrawnReporterOwner‘

Android Studio新建项目就报错&#xff1a; Cannot access ‘androidx.activity.FullyDrawnReporterOwner’ which is a supertype of ‘cn.dazhou.osddemo.MainActivity’. Check your module classpath for missing or conflicting dependencies 整个类都报错了。本来原来一直…

文献学习-37-动态场景中任意形状针的单目 3D 位姿估计:一种高效的视觉学习和几何建模方法

On the Monocular 3D Pose Estimation for Arbitrary Shaped Needle in Dynamic Scenes: An Efficient Visual Learning and Geometry Modeling Approach Authors: Bin Li,† , Student Member, IEEE, Bo Lu,† , Member, IEEE, Hongbin Lin, Yaxiang Wang, Fangxun Zhong, Me…

使用arthas查看java项目resources目录下面的文件内容

有一次在测试环境想看resources下面的mapper文件内容&#xff08;代码执行和预期不一致&#xff0c;所以想排查一下是不是打上去的包有问题&#xff0c;没有通过下载jar的方式解压查看&#xff09;&#xff0c;然后想到了使用arthas来弄&#xff0c;这里记录一下怎么个查看法。…
最新文章