当前位置:   article > 正文

c++20协程基础概念_c++ 协程

c++ 协程

c++协程介绍

前言

官方文档地址
本文主要对c++reference做翻译(不会逐字翻译),同时对其中的概念以及协程运行过程做对应的解释。因为是学习过程中的记录,如有问题,希望大家能够指正,谢谢。也欢迎提问,若看到了一定会第一时间回复,本文会持续更新,保证知识的正确性。

协程(coroutine)

定义

首先介绍什么是协程,协程是一个能够挂起执行流稍后恢复执行的函数。c++20协程是无栈的:它通过主动返回结果给caller来挂起执行流;恢复执行流继续执行所需要的数据将被存储在堆上。协程的一个重要优点就是能够将异步执行转化为同步执行(如各种回调,会导致代码割裂而难以阅读)。
下面介绍几个关键字
co_await,co_yield,co_return

  • co_await : 挂起协程直到恢复
  • co_yield : 挂起协程的同时返回一个值
  • co_return:结束协程执行流的同时返回一个值
    这里重点理解下co_await和co_yield的区别,co_await expression ,只有在resume后我们才能得到expression的返回值,但co_yield expression,是在挂起的同时返回expression的值。两者最大的区别就是,前者expression的返回值无法作为参数递到下一次挂起的协程,而后者可以。具体示例将在下一篇文章出,这里暂留一个印象即可。

限制

协程不能够使用变长参数,原生的return,或者自动类型推导符auto或者concept,以及const 函数,常量表达式,构造函数,析构函数以及main函数。

执行流

协程由下面几个对象组成。

  • promise object : 该对象从协程内部被操作,协程通过它来提交result和exception。
  • coroutine handle:该对象在协程外部被操作,用来恢复协程执行流和销毁协程,该handle不属于任何协程
  • coroutine state: 该对象分配在堆上,其中包含了下面:

(1) promise object
(2) 函数参数(按值拷贝,注意引用实际上是指针,所以拷贝的是其对象的地址)
(3) 生命周期跨越挂起点的局部变量和临时变量(如 awaiter 对象)

执行过程
  • 使用运算符new创建coroutine state
  • 按值拷贝所有函数参数到coroutine state中,所以注意指针形参以及引用形参,可能协程恢复执行时,其指向的对象已经被释放了 ,使用时会coredump
  • 调用构造函数初始化promise object。如果promise type 的构造函数包括了所有的协程参数,那么就会调用这个构造函数,否则则调用默认构造函数。类比普通类的构造函数调用规则。
  • 调用promise.get_return_object() , 然后保存结果在一个局部变量中(具体是哪个还没研究,后面也没讲),在协程第一次挂起时,将这个结果返回给caller。
  • 调用promise.initial_suspend(),然后co_await result (这个暂时不清楚干了什么,编译器应该在这里添加了许多额外代码,后续研究)。一个经典的Promise type 既可以返回std::suspend_always,也可以返回std::suspend_never。前者实际上返回的未false,后者返回未true,影响是:前者co_await后当前执行流就不继续往下走了( lazily-started coroutines),而后者当前协程执行流在挂起目标协程后,会继续往下走(eagerly-started coroutines)。
  • 当co_await promise.initial_suspend() (挂起点)resume(恢复执行),就从当前挂起点开始执行当前协程执行流。如果promise type 返回suspend_always,则需要在挂起点恢复后,继续往下执行,如果未suspend_never的,则立马向下执行

至此上面讲述了协程运行至,co_await expression 的过程,假设co_await挂起的点恢复了,继续向下执行,那下面我们看看继续看看协程怎么结束的。

协程结束有两种方式一种是运行到co_return,另一种方式是抛出异常

运行到co_return

  • 调用 promise.return_void()

    co_return
    co_return expr
    执行完函数体。这种方式,如果没有Promise::return_void(),返回结果将是未定义的。

  • 或者 call promise.return_value(expr) for co_return expr
  • 按照局部变量存储生命周期的逆序释放完所有局部变量
  • 调用 promise.final_suspend() ,返回结果。如果在该点恢复协程,其结果将是未定义的,原因很简单,因为之前保存的局部变量被释放了。

未被捕获的异常
协程也可以通过异常结束,因为它被coroutine handle摧毁了。

  • 调用promise object的析构函数析构对象
  • 调用function parameter 的析构函数,析构拷贝的对象。
  • 调用delete 释放coroutine state 使用的内存
  • 转移执行流给caller/resumer

堆分配

coroutine state 通过new 运算符分配在堆内存上。如果promise type类内重载了new运算符,则不会使用全局的new运算符(这里我们是否可以让其分配在我们的内存池上)
如果我们重写了带参数的new 操作运算符,它的参数列表中第一个参数是一个需要分配内存的大小,剩余为协程函数的形参。该分配满足下面两点可能被优化,从而不分配在堆上:

  • 生命周期严格对齐caller
  • 协程栈的大小caller可知

这种情况下,coroutine state将会嵌入到caller的栈帧中(满足编译器的设计的原则,尽可能使用栈内存),如果caller是一个协程,则嵌入到caller的coroutine state 中。

既然分配内存会使用到new,那么自然可能会出现动态内存分配失败的问题。

如果分配失败了,协程会抛出 std::bad_alloc,除非promise type 定义了Promise::get_return_object_on_allocation_failure()。
如果该函数被定义,allocation将会使用不抛出异常的new,当分配失败时,协程就会立刻返回Promise::get_return_object_on_allocation_failure()中获取的对象给caller。

promise

单目运算符co_await将挂起一个协程,然后返回控制权给caller。co_await的操作数是后面的expression,该expression需要定义操作运算符co_await或者实现了Promise::await_transform将类型转换为caller的协程类型。

co_await expression
执行细节

  • 首先,expression 将会被转换为一个可等待对象(awaiter),步骤如下:
  • 然后,将会调用awaiter.await_ready() (该函数是个便捷方式,如果已知结果已经准备好了或者能够完全同步地完成,则可以避免挂起的开销)。
    如果函数返回的结果是false 则:
    该协程会被挂起(指代为exression协程,他的协程状态由局部变量和当前挂起点组成),随后将会调用awaiter.await_suspend(handle) ,该传入handle,代表当前协程。在awaiter.await_suspend(handle)中,该函数可以通过handle来看到挂起协程的coroutine state,所以我们暂时可以得出结论awaiter.await_suspend(handle)前,coroutine state已经创建完毕。awaiter.await_suspend(handle),因此有义务将协程的恢复交给executor调度(schedule),或者销毁协程(该函数返回值为false,则说明该协程正在调度中)

    如果await_suspend返回void,将立刻转移控制权给caller/resume,即外层协程。该协程仍然挂起
    如果await_suspend返回bool类型:
    (1)返回true,说明还未调度,则归还控制权给caller
    (2)返回false,说明正在调度中,即立刻resume当前协程
    如果await_suspend返回一个协程句柄coroutine handle,则该handle将用来resume该协程(如handle.resume)。这种方法将最终导致链式恢复。
    如果await_suspend中抛出了异常,且在内部被捕获。则在协程恢复后(resume),异常将立刻再次抛出。

+最后,无论协程是否挂起,都会调用awaiter.await_resume(),其结果将是整个co_await expression 表达式的返回值。

如果协程在 co_await expression中被挂起,则恢复点将在该awaiter.await_resume()之前。

【注意】由于在进入awaiter.await_suspend()前,协程已经完全挂起了,所以跨线程传递coroutine handle是线程安全的,无需其他的同步。比如,我们可以将其传入一个回调函数中,并将该回调交给线程池处理后续逻辑处理。注意传递是线程安全的,而执行可能不是。假设在主线程A中我们呢调用co_await expression 创建了一个awaiter,然后在awaiter.await_suspend(handle)中,将handle交给了线程池,则后续执行resume时,需要小心再A线程中resume过了。所以我们的原则是:对待*this为已经被销毁了,再分配给其他线程后不要再access该协程。

【注意】协程对象是coroutine state 的一部分。作为一个临时变量,它的生命周期跨越一整个挂起点,它将会在co_await expression 完成前销毁。

代码分析

至此reference翻译完了。接下来我们结合代码来看吧。

#define __cpp_lib_coroutine

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

using namespace std::chrono_literals;


// 这个是用来做函数协程返回的
struct Result {
  struct promise_type {
    std::suspend_never initial_suspend() {
      std::cout << "Result::initial_suspend" << std::endl;
      return {};
    }

    std::suspend_never  final_suspend() noexcept  {
      std::cout << "Result::final_suspend" << std::endl;
      return {};
    }

    Result get_return_object() {
      std::cout << "Result::get_return_object" << std::endl;
      return {};
    }

    void return_void() {

    }

    void unhandled_exception() {

    }
  };
};

struct Result2 {
  struct promise_type {
    std::suspend_never initial_suspend() {
      std::cout << "Result2::initial_suspend" << std::endl;
      return {};
    }

    std::suspend_never final_suspend() noexcept  {
      std::cout << "Result2::final_suspend" << std::endl;
      return {};
    }

    Result2 get_return_object() {
      std::cout << "Result2::get_return_object" << std::endl;
      return {};
    }

    void return_void() {

    }

    void unhandled_exception() {

    }
  };
};


Result2 Coroutine2(int * value) {
  std::cout << "Coroutine2" << std::endl;
  std::cout << *value << std::endl;
  co_return;
}

Result Coroutine(int * value) {
  std::cout << "Coroutine" << std::endl;
  std::cout << *value << std::endl;
  Coroutine2(value);
  std::cout << "Coroutine end" << std::endl;
  co_return;
};

int main() {
  int num = 99;
  Coroutine(&num);
  std::cout << "end program" << std::endl;
  return 0;
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87

输出:

Result::get_return_object
Result::initial_suspend
Coroutine
99
Result2::get_return_object
Result2::initial_suspend
Coroutine2
99
Result2::final_suspend
Coroutine end
Result::final_suspend
end program

分析:

定义了两种协程,且initial_suspend,final_suspend返回均为std::suspend_never。这种其实就和普通函数没有区别,且在这种情况下coroutine state将嵌入到栈帧中去,而不是分配在堆空间上,所以不需要用指针去访问对象,效率更高

接下来我们将对上述代码进行修改
在这里插入图片描述
在这里插入图片描述

输出:

Result::get_return_object
Result::initial_suspend
end program

分析:
这里我们发现协程中所有都没有打印,这里的原因是:普通函数main调用了couroutine,协程initial_suspend后返回suspend_always,所以创建了result这个协程对象并挂起,但是普通函数继续往下执行直至进程生命周期结束,也没有resume。

在这里插入图片描述

在这里插入图片描述
输出:

Result::get_return_object
Result::initial_suspend
Coroutine
99
Result2::get_return_object
Result2::initial_suspend
Coroutine end
Result::final_suspend
end program

分析:

分析上述结果我们可以知道当协程coroutine运行到Coroutine2(value) 时,创建result协程对象并挂起,所以不向之前普通函数调用协程那样直接运行后面的语句。

suspend_never

标准库中的awaiter object,结构如下:
在这里插入图片描述
我们可以用自己的类来替代,继续看下这几个函数的调用规则和机制。

struct suspend
{
  bool await_ready() const noexcept { 
    std::cout << "await_ready" << std::endl;
    return false; 
  }

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

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


struct Result {
  struct promise_type {
      suspend initial_suspend() {
        std::cout << "Result::initial_suspend" << std::endl;
        return {};
      }

      suspend final_suspend() noexcept  {
        std::cout << "Result::final_suspend" << std::endl;
        return {};
      }

      Result get_return_object() {
        std::cout << "Result::get_return_object" << std::endl;
        return {};
      }

      void return_void() {

      }

      void unhandled_exception() {

      }
  };
};


Result Coroutine(int * value) {
  std::cout << "Coroutine" << std::endl;
  std::cout << *value << std::endl;
  std::cout << "Coroutine end" << std::endl;
  co_return;
};

int main() {
  int num = 99;
  Coroutine(&num);
  std::cout << "end program" << std::endl;
  return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58

输出:

Result::get_return_object
Result::initial_suspend
await_ready
await_suspend
end program

分析

这次我们关注await_*。当await_ready返回false,则说明还没准备好resume,接着调用await_suspend,如果返回void,则直接将控制权归还给调用函数,然后又因为数据没有准备好,所以理所应当不执行resume。

在这里插入图片描述
输出

在这里插入图片描述

分析

当返回值为true时,则直接进行resume,将不会去根据await_suspend判断协程是否可立即调度。

经过上面,我们可以确定,当await_suspend返回true时,就会忽略await_suspend直接去调await_resume了。下面我们继续看下await_suspend返回false的两种情况。

在这里插入图片描述
输出

Result::get_return_object
Result::initial_suspend
await_ready
await_suspend
end program

分析
可见,当未准备好,且不可立即调度(await_suspend 返回false 可以理解为,该awaiter 外的线程对象result这在调度中,所以可以立即执行await_resume)

在这里插入图片描述
输出
在这里插入图片描述

分析
协程对象result,能数据没准备好,但是正在调度中,所以可以立即执行await_resume 来恢复执行。

再分析下本次结果
在这里插入图片描述
分析

如上就是一个协程挂起和恢复的全部过程:

  • promise_type对象调用get_return_object创建协程对象result
  • 调用协程对象result的initial_suspend
    由于awaiter对象suspend此时未false,true,可以直接resume 协程对象result
    所以执行协程体coroutine
    执行完毕后调用final_suspend,由于该函数也会返回suspend,所以还会调用一边awaiter的流程

执行完resume,result对象就释放了,suspend作为result的局部对象,也会释放掉。suspend可以反映协程对象的状态。

co_await

至此我们还没有使用过co_await,既然不用co_await协程依旧可以挂起,那为什么还要使用co_await呢?下面我们对比下使用co_await的区别

取消注释更改代码结构如下:

#define __cpp_lib_coroutine

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

using namespace std::chrono_literals;

struct suspend
{
  bool await_ready() const noexcept { 
    // std::cout << "await_ready" << std::endl;
    return true; 
  }

  bool await_suspend(std::coroutine_handle<>) const noexcept {
    // std::cout << "await_suspend" << std::endl;
    return true;
  }

  void await_resume() const noexcept {
    // std::cout << "await_resume" << std::endl;
  }
};


struct Result {
  struct promise_type {
      suspend initial_suspend() {
        std::cout << "Result::initial_suspend" << std::endl;
        return {};
      }

      suspend final_suspend() noexcept  {
        std::cout << "Result::final_suspend" << std::endl;
        return {};
      }

      Result get_return_object() {
        std::cout << "Result::get_return_object" << std::endl;
        return {};
      }

      // void operator co_await(){
      //   std::cout << "co_await" << std::endl;
      // }
      
      void return_void() {

      }

      void unhandled_exception() {

      }
  };
};

suspend Coroutine2(int * value) {
  std::cout << "Coroutine2" << std::endl;
  // std::cout << *value << std::endl;
  // std::cout << "Coroutine2 end" << std::endl;
  return {};
};

Result Coroutine(int * value) {
  std::cout << "Coroutine" << std::endl;
  // std::cout << *value << std::endl;
  Coroutine2(value);
  std::cout << "Coroutine end" << std::endl;
  co_return;
};

int main() {
  int num = 99;
  Coroutine(&num);
  std::cout << "end program" << std::endl;
  return 0;
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80

修改下面代码
在这里插入图片描述
经过测试我们发现:

  • 当await_ready=false,await_suspend=false时两者输出一致,说明使用co_await和不使用co_await一样
  • 当await_ready=true,await_suspend=false/true; await_ready=false,await_suspend=fase,时两者输出不一致,那原因时什么呢?是因为类函数式调用情况下协程被优化了吗?是由于显示调用了co_await,编译器就没有将其优化掉吗?有时间看汇编了再回来填坑。
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/花生_TL007/article/detail/208395
推荐阅读
相关标签
  

闽ICP备14008679号