C/C++开发,无可避免的多线程(篇三).协程及其支持库

news2024/12/27 11:53:38

一、c++20的协程概念

        在c++20标准后,在一些函数中看到co_await、co_yield、co_return这些关键词,这是c++20为协程实现设计的运算符。

        协程是能暂停执行以在之后恢复的函数。原来我们调用一个功能函数时,只要调用了以后,就要完整执行完该功能函数所有步骤(语句)才能回来执行自身的步骤,对于一些功能函数其由很长的执行周期,该执行周期中调用者就不能处理自身一些事务。在协程出现以前我们就需要回调、阻塞等手段综合设计实现。

        协程就是为了解决类似这种问题的。调用者调用协程函数后,执行到中途可以通过co_yield暂停挂起,返回自身执行事务,然后在通过resume唤醒恢复协程,协程函数会从挂起标识处继续往下执行。整个协程函数执行周期内,可以多次返回调用自身。在协程函数结束后,通过co_return还可以返回协程结果或协程内部对象。

        协程是无栈的:它们通过返回到调用方暂停执行,并且从栈分离存储恢复执行需要的数据。这样就可以编写异步执行的顺序代码(例如,不使用显式的回调来处理非阻塞 I/O),还支持对惰性计算的无限序列上的算法及其他用途。 

        例如用关键词 co_yield 暂停执行并返回一个值:

task coroutine_func(int n = 1) 
{
    int i = 0;
    while(i<n){
        co_yield i++;
    }
}

        或者用关键词 co_return 完成执行并返回一个值或void:

task coroutine_func(int n = 1) 
{
    co_return; 
}

二、c++20协程库

        这些关键词都做了啥事情呢。在c++20协程库中,提供了以下支持库来实现协程:

协程特征,定义于头文件 <coroutine>
coroutine_traits        (C++20)用于发现协程承诺类型的特征类型(类模板) 

协程柄,定义于头文件 <coroutine>
coroutine_handle        (C++20)用于指代暂停或执行的协程(类模板) 

无操作协程,定义于头文件 <coroutine>
noop_coroutine          (C++20)创建在等待或销毁时无可观察作用的协程柄(函数) 
noop_coroutine_promise  (C++20)用于无可观察作用的协程(类) 
noop_coroutine_handle   (C++20)std::coroutine_handle<std::noop_coroutine_promise> ,有意用于指代无操作协程
(typedef) 

平凡可等待体,定义于头文件 <coroutine>
suspend_never           (C++20)指示 await 表达式应该决不暂停(类) 
suspend_always          (C++20)指示 await 表达式应该始终暂停(类) 

         std::coroutine_traits从协程的返回类型与形参类型确定承诺类型。

//定义于头文件 <coroutine> ,(C++20 起) 
template< class R, class... Args > struct coroutine_traits;

/*模板形参
*R - 协程的返回类型
*Args - 协程的形参类型,若协程为非静态成员函数则包括隐式对象形参
*/

        标准库实现提供与 R::promise_type 相同的公开可访问成员类型 promise_type ,若该有限定标识合法并代表类型。否则它无成员。coroutine_traits 的程序定义特化应当定义公开可访问的成员类型 promise_type ,否则行为未定义。

//成员类型
//类型             定义
promise_type         R::promise_type     //若它合法,或由程序定义特化提供

         2.1 coroutine_handle句柄

        其中最主要的就是coroutine_handle句柄,类模板 coroutine_handle 能用于指代暂停或执行的协程,定义于头文件 <coroutine>:

//(C++20 起) 
/*结构体主模板,可从 Promise 类型的承诺对象创建。*/
template< class Promise = void > struct coroutine_handle;       

/*特化std::coroutine_handle<void>擦除承诺类型。它可从其他特化转换*
template<> struct coroutine_handle<void>;  

/*特化std::coroutine_handle<std::noop_coroutine_promise>指代无操作协程。不能从承诺对象创建它*/                     
template<> struct coroutine_handle<std::noop_coroutine_promise>;
using noop_coroutine_handle =
     std::coroutine_handle<std::noop_coroutine_promise>;

        std::coroutine_handle 的每个特化均为可平凡复制 (TriviallyCopyable) ,并保有一个指向协程状态的指针作为其仅有的非静态成员。添加 coroutine_handle 的特化的程序行为未定义。std::coroutine_handle 功能如下:

//(C++20) 
成员函数
(构造函数)     构造 coroutine_handle 对象(公开成员函数) 
operator=     赋值 coroutine_handle 对象(公开成员函数) 
from_promise    [静态]从协程的承诺对象创建 coroutine_handle(公开静态成员函数) 

转换
operator coroutine_handle<> 获得擦除类型的 coroutine_handle(公开成员函数) 

观察器
done             检查协程是否已完成(公开成员函数) 
operator bool    检查柄是否表示协程(公开成员函数) 

控制
operator()
resume           恢复协程执行(公开成员函数) 
destroy          销毁协程(公开成员函数) 

承诺访问
promise          访问协程的承诺对象(公开成员函数) 

导出/导入
address         导出底层地址,即支撑协程的指针(公开成员函数) 
from_address    [静态]从指针导入协程(公开静态成员函数) 

非成员函数
operator==      比较二个 coroutine_handle 对象(函数) 
operator<=>     比较二个 coroutine_handle 对象(函数) 

辅助类
std::hash<std::coroutine_handle>  std::coroutine_handle 的散列支持(类模板特化) 

        std::coroutine_handle,协程句柄是从协程外部操纵的,这是用于恢复协程执行或销毁协程帧的非拥有柄;承诺(promise)对象,从协程内部操纵,协程通过此对象提交其结果或异常。

        2.2 std::coroutine_handle实现案例

        现在来看如何通过std::coroutine_handle实现协程函数的,下面定义一个简单的协程例子:

//test0.h
#ifndef _TEST_0_H_
#define _TEST_0_H_
void coroutine_first_test(void);
#endif //_TEST_0_H_
//test0.cpp
#include "test0.h"

#include <coroutine>
#include <iostream>

struct task {
    struct promise_type {
        task get_return_object() { 
            std::cout << "task::promise_type.get_return_object \n";
            return task{Handle::from_promise(*this)}; 
        }
        //返回std::suspend_never(这个随后说明) ,初始化后就继续运行
        std::suspend_never initial_suspend() { 
            std::cout << "task::promise_type.initial_suspend \n";
            return {}; 
        }
        std::suspend_never final_suspend() noexcept { 
            std::cout << "task::promise_type.final_suspend \n";
            return {}; 
        }
        std::suspend_always yield_value(const int &val) noexcept { //co_yield调用
            std::cout << "task::promise_type.yield_value " << val << "\n";
            return {};
        }
        void return_void() {}   //co_return调用
        void unhandled_exception() {
            std::cout << "task::promise_type.unhandled_exception \n";
        }
    };
    using Handle = std::coroutine_handle<promise_type>;//协程句柄
    explicit task(Handle coroutine) : m_coroutine{coroutine} {} //get_return_object时调用
    task() = default;
    ~task() { 
        std::cout << "~task \n";
        if (m_coroutine) //自行销毁
        {
            m_coroutine.destroy(); 
        }
    }
    // task(const task&) = delete;
    // task& operator=(const task&) = delete;
    Handle m_coroutine; //协程句柄
};

task coroutine_func(int n = 0) 
{
    int i = 0;
    while(i<n){
        co_yield i++;
        std::cout << "coroutine dosomthing" << i << "\n";
    }
    co_return; 
}

void coroutine_first_test(void)
{
    auto c0_obj = coroutine_func(10);
    for (size_t i = 0; i < 5; i++)
    {
        c0_obj.m_coroutine.resume();//唤醒协程
        std::cout << "caller dosomthing" << i << "\n";
    }
};
//main.cpp
#include "test0.h"

int main(int argc, char* argv[])
{
    coroutine_first_test();
    return 0;
};

        task是个自定义的结构体,为了能作为协程的返回值,需要定义一个内部 promise_type结构体。

        【1】协程开始执行时,它进行下列操作:

  1. 用 operator new 分配协程状态对象(coroutine state)。
  2. 将所有函数形参复制到协程状态中:按值传递的形参被移动或复制,按引用传递的参数保持为引用(因此,如果在被指代对象的生存期结束后恢复协程,它可能变成悬垂引用),上述例如传入的是int型引用。
  3. 调用承诺对象的构造函数(promise)。如果承诺类型拥有接收所有协程形参的构造函数,那么以复制后的协程实参调用该构造函数。否则调用其默认构造函数。这里采用默认构造函数,没具名给出。
  4. 调用 promise.get_return_object() 并将其结果在局部变量中保持。该调用的结果将在协程首次暂停时返回给调用方。至此并包含这个步骤为止,任何抛出的异常均传播回调用方,而非置于承诺中。
  5. 调用 promise.initial_suspend() 。典型的承诺类型要么(对于惰性启动的协程)返回 std::suspend_always,要么(对于急切启动的协程)返回 std::suspend_never。
  •         *如果返回std::suspend_never,表示await表达式应该决不暂停,立即执行,不挂起
  •         *返回std::suspend_always,表示await表达式应该始终暂停,不立即执行,先挂起
  1. 当 co_await promise.initial_suspend() 恢复时,开始协程体的执行。

        【2】当协程抵达暂停点时:

  1. 将先前获得的返回对象返回给调用方/恢复方,这里是coroutine_first_test函数,如果需要则先隐式转换到协程的返回类型。
  2. 协程从暂停点返回通过co_yield,本质上是调用了promise.yield_value(表达式)
  3. 调用方/恢复方,coroutine_first_test函数通过调用coroutine_handle的resume告知协程恢复执行,协程函数coroutine_func重新从co_yield语句的下一句开始执行。

        【3】当协程抵达 co_return 语句时,它进行下列操作:

  1. 若是co_return,调用 promise.return_void(),如果承诺类型 Promise 没有 Promise::return_void() 成员函数(本例所采用),那么则行为未定义。
  2. 若是co_return expr,调用 promise.return_value(expr),其中 expr 具有非 void 类型或 void 类型,如果承诺类型 Promise 没有 Promise::return_value() 成员函数(本例子没定义),那么则行为未定义。
  3. 控制流出返回时,协程结束开始结束运行。此时以创建的逆序销毁所有具有自动存储期的变量。
  4. 调用 promise.final_suspend() 。

        【4】如果协程因未捕捉的异常结束,那么它进行下列操作:

  1. 捕捉异常并在 catch 块内调用 promise.unhandled_exception()
  2. 调用 promise.final_suspend() (例如,以恢复某个继续或发布其结果)。此时开始恢复协程是未定义行为。

        【5】协程当经由 co_return 或未捕捉异常而正常终止,它进行下列操作:

  1. 调用承诺对象的析构函数(~promise_type,默认析构)。
  2. 调用各个函数形参副本的析构函数(本例只有int型引用)。
  3. 调用 operator delete 以释放协程状态所用的内存(~task)。
  4. 转移执行回到调用方/恢复方(coroutine_first_test)。

         这里的协程函数返回对象采用的是 std::suspend_never等待体,标准库定义了两个平凡的可等待体:std::suspend_always 及 std::suspend_never,先说std::suspend_never:

//std::suspend_never,定义于头文件 <coroutine>,
//suspend_never 是空类,能用于指示 await 表达式绝不暂停并且不产生值。

/*成员函数*/
/*(C++20 起) 
*std::suspend_never::await_ready,指示 await 表达式绝不暂停(公开成员函数) 
*始终返回 true ,指示 await 表达式绝不暂停。
*/
constexpr bool await_ready() const noexcept { return true; }
  
/*(C++20 起) 
*std::suspend_never::await_suspend,无操作(公开成员函数) 
*不做任何事。
*/   
constexpr void await_suspend() const noexcept {}
  
/*(C++20 起) 
*std::suspend_never::await_resume,无操作(公开成员函数) 
*不做任何事。若使用 suspend_never 则 await 表达式不产生值。
*/
constexpr void await_resume() const noexcept {} 

        编译g++ main.cpp test*.cpp -o test.exe -std=c++20,运行程序很好展示了上述逻辑过程:

         2.3 承诺类型(Promise)

        承诺类型(Promise),获得到承诺对象的引用。若 *this 不指代承诺对象尚未被销毁的协程,则行为未定义。此函数不对特化 std::coroutine_handle<> 提供。

//std::coroutine_handle<Promise>::promise
//主模板的成员
Promise& promise() const;
//特化 std::coroutine_handle<std::noop_coroutine_promise> 的成员
std::noop_coroutine_promise& promise() const noexcept; 

        编译器用 std::coroutine_traits 从协程的返回类型确定承诺类型。

正式而言,,如果定义它为非静态成员函数,以如下方式确定它的承诺类型 :

/*
*令 R 与 Args... 分别代表协程的返回类型与参数类型列表,
*ClassT 与 /*cv限定*/ (如果存在)分别代表协程所属的类与其 cv 限定
*/
std::coroutine_traits<R, Args...>::promise_type    //如果不定义协程为非静态成员函数。
std::coroutine_traits<R, ClassT /*cv限定*/&, Args...>::promise_type    //如果定义协程为非右值引用限定的非静态成员函数。
◦std::coroutine_traits<R, ClassT /*cv限定*/&&, Args...>::promise_type  //如果定义协程为右值引用限定的非静态成员函数。

        例如,如果上述结构体task定义为结构体模板,template<typename T>  struct task,其协程函数定义:

//如果定义协程为 
task<float> foo(std::string x, bool flag);
//那么它的承诺类型是 
std::coroutine_traits<task<float>, std::string, bool>::promise_type。

//如果定义协程为 
task<void> my_class::method1(int x) const;
//那么它的承诺类型是 std::coroutine_traits<task<void>, const my_class&, int>::promise_type。

//如果定义协程为 
task<void> my_class::method1(int x) &&;
//那么它的承诺类型是 
std::coroutine_traits<task<void>, my_class&&, int>::promise_type。

        每个协程均与下列对象关联:

  • 承诺(promise)对象。
  • 协程句柄 (coroutine handle)。
  • 协程状态 (coroutine state),它是一个包含以下各项的分配于堆(除非优化掉其分配)的内部对象:
  1. 承诺对象
  2. 各个形参(全部按值复制)
  3. 当前暂停点的某种表示,使得恢复时程序知晓要从何处继续,销毁时知晓有哪些局部变量在作用域内
  4. 生存期跨过当前暂停点的局部变量和临时量

        协程状态由非数组 operator new 在堆上分配。如果承诺类型定义了类级别的替代函数,那么会使用它,否则会使用全局的 operator new;如果承诺类型定义了接收额外形参的 operator new 的布置形式,且它们所匹配的实参列表中的第一实参是要求的大小(std::size_t 类型),而其余则是各个协程函数实参,那么将这些实参传递给 operator new(这使得能对协程使用前导分配器约定)

        如果分配失败,那么协程抛出 std::bad_alloc,除非承诺类型 Promise 类型定义了成员函数 Promise::get_return_object_on_allocation_failure()。如果定义了该成员函数,那么使用 operator new 的 nothrow 形式进行分配,而在分配失败时,协程会立即将从 Promise::get_return_object_on_allocation_failure() 获得的对象返回给调用方。

        2.4 承诺类型-协程返回类型及协程函数的交互

        下来看一下一个更复杂的例子,可以从协程传递( co_yield、co_return)回引用,实现协程与调用者的交互。

//test1.h
#ifndef _TEST_1_H_
#define _TEST_1_H_
void coroutine_model_test(void);
#endif //_TEST_1_H_
//test1.cpp
#include "test1.h"

#include <coroutine>
#include <iostream>
#include <optional>
#include <ranges>
 
template<typename T> requires std::movable<T>
class Task {
public:
    //promise_type就是承诺对象,承诺对象用于协程内外交流
    struct promise_type {
        //生成协程返回,会在协程正在运行前进行调用
        Task<T> get_return_object() {
            std::cout << "get_return_object \n";
            return Task{Handle::from_promise(*this)};
        }
        /*
        *返回的就是std::suspend_always,在协程被创建及真正运行前,被调用
        */
        static std::suspend_always initial_suspend() noexcept {
            std::cout << "initial_suspend \n";
            return {}; 
        }
        //返回awaiter,在协程最后退出后调用的接口。
        static std::suspend_always final_suspend() noexcept { 
            std::cout << "final_suspend \n";
            return {}; 
        }
        //返回awaiter,会在 co_yield v 时被调用类型就是T,v就是传入参数value
        std::suspend_always yield_value(T value) noexcept //-4-
        {
            current_value = std::move(value);
            std::cout << "yield_value ";
            return {};
        }
        //会在 co_return v 时被调用,把 co_return 后面跟着的值value作为参数传入,这里一般就是把这个值保存下来,提供给协程调用者
        void return_value(const T& value)
        {
            std::cout << "return_value call "<< value << "\n";//cout危险操作,取决于T类型,这里为了展示原理
            current_value = std::move(value);
            return;
        }
        //会在 co_return v 时被调用,无传入值,和return_value只选一个,否则会报编译错误
        /*
        void return_void()
        {
            std::cout << "return void invoked." << std::endl;
        }
        */
        // 生成器协程中不允许 co_await 。
        void await_transform() = delete;
        //协程内的代码抛出了异常,这个接口会被调用
        static void unhandled_exception() {
            std::cout << "unhandled_exception ";
            throw;
        }
 
        std::optional<T> current_value;
    };
 
    using Handle = std::coroutine_handle<promise_type>;//协程句柄
 
    explicit Task(Handle coroutine) : 
        m_coroutine{coroutine}
    {}
 
    Task() = default;
    ~Task() { 
        std::cout << "~Task \n";
        if (m_coroutine) //自行销毁
        {
            m_coroutine.destroy(); 
        }
    }
 
    Task(const Task&) = delete;
    Task& operator=(const Task&) = delete;
 
    Task(Task&& other) noexcept : 
        m_coroutine{other.m_coroutine}
    { 
        other.m_coroutine = {}; 
    }
 
    Task& operator=(Task&& other) noexcept {
        if (this != &other) {
            m_coroutine = other.m_coroutine;
            other.m_coroutine = {};
        }
        return *this;
    }
 
    // 基于范围的 for 循环支持。通过迭代操作实现协程应用
    class Iter {
    public:
        void operator++() //-6-
        { 
            std::cout << "real resume ";
            m_coroutine.resume(); //++时,恢复协程
        }
        const T& operator*() const { 
            return *m_coroutine.promise().current_value; //取值,通过promise获取数据,返回值T
        }        
        bool operator==(std::default_sentinel_t) const { 
            return !m_coroutine || m_coroutine.done(); //赋值时,协程执行
        }
 
        explicit Iter(Handle coroutine) : 
            m_coroutine{coroutine}
        {}
 
    private:
        Handle m_coroutine;//协程句柄
    };
    //range应用指定的开闭区间
    Iter begin() {
        if (m_coroutine) {
            m_coroutine.resume();
        } 
        return Iter{m_coroutine};
    }
    std::default_sentinel_t end() { 
        return {}; 
    }
    const T& get_val()
    {
        return *m_coroutine.promise().current_value;
    }
private:
    Handle m_coroutine; //协程句柄
};

//协程函数的返回值为Task<T>类型,协程的返回类型必须内部定义Task<T>::promise_type
template<std::integral T>
Task<T> range(T first, T last)     
{
    T sum = T();
    while (first < last) //-2-
    {
        sum += first;
        co_yield first++;//协程会挂起,返回值;等价于co_await promise.yield_value(表达式)。-3-
        //调用者resume时,在此处恢复执行
        std::cout << "co_yield\n";//-7-
    }
    co_return sum;//等价于co_await promise.return_value(表达式)
};
 
void coroutine_model_test(void)
{
    auto rs = range(-4, 4);
    for (int i : rs) //-1-,
    // for (int i : range(-4, 4)) //-1-,
    {
        std::cout << i << " ";//-5-
    }
    std::cout << "\n";
    std::cout << "rs last val = " << rs.get_val() << "\n";
};
//main.cpp
#include "test1.h"

int main(int argc, char* argv[])
{
    coroutine_model_test();
    return 0;
};

       本案例定义了一个for循环的范围返回函数,该范围是Task类的begin()和end()函数提供,而函数引用了内置类型Iter,Iter在迭代递增时(operator++()),会调用std::coroutine_handle的resume进行协程恢复。协程运行到“co_yield first++;”时,就会通过yield_value设置了promise内部的缓存值,并返回,而调用者函数coroutine_model_test则通过std::coroutine_handle句柄获知promise承诺对象及内部值(及遍历数值),在遍历时,每次递增,本质上会调用std::coroutine_handle的resume告知协程恢复执行,而协程每次进行递增数值会写入promise承诺对象内部,如此反复等同于coroutine_model_test获得遍历范围值。

        协程最终返回时“co_return sum;”,传递回来一个数值,本质上是通过promise承诺对象内部return_value函数实现的。因为通过“current_value = std::move(value);”将最后传递进入的值保存在promise承诺对象内部,因此在调用函数内通过get_val就能取得该缓存的值。

    const T& Task<T>::get_val()
    {
        return *m_coroutine.promise().current_value;
    }

         上述例子中通过“-*-”标识了协程调用逻辑次序,编译g++ main.cpp test*.cpp -o test.exe -std=c++20,运行测试:

         Task内部promise 类的工作主要是两个:

  1. 从协程的承诺对象创建 coroutine_handle,接口是get_return_object。
  2. 是定义协程的执行流程,主要接口是initial_suspend,final_suspend。
  3. 是负责协程和调用者之间的数据传递,主要接口是 yield_value 和return_value或return_void。

        通常,promise_type类型需要主要实现这几个接口:

        【1】Task<T> get_return_object () 这个接口要能用 promise 自己的实例构造出一个协程的返回值,会在协程正在运行前进行调用,这个接口的返回值会作为协程的返回值。

        【2】std::suspend_always initial_suspend () 这个接口会在协程被创建(也就是第一次调用),真正运行前,被调用。在上述这个例子里,指定返回空类,指示 await 表达式始终暂停并且不产生值。

return {}; 

        std::suspend_always是一个结构体,前面已经说明了std::suspend_never,下来看看std::suspend_always,它和std::suspend_never几乎一样:

/* (C++20 起) 
*std::suspend_always,定义于头文件 <coroutine>
*suspend_always 是空类,能用于指示 await 表达式始终暂停并且不产生值。
*/
struct suspend_always;

        该类包含几个成员函数,用来判定

成员函数
/*(C++20)  指示 await 表达式始终暂停(公开成员函数) 
*std::suspend_always::await_ready
*始终返回 false ,指示 await 表达式始终暂停。
*/
constexpr bool await_ready() const noexcept { return false; }
   
/*(C++20)  无操作(公开成员函数)
*std::suspend_always::await_suspend
*不做任何事。
*/
constexpr void await_suspend() const noexcept {}
  (C++20 起) 
   
/*(C++20)  无操作(公开成员函数) 
*std::suspend_always::await_resume
*不做任何事。若使用 suspend_always 则 await 表达式不产生值。
*/
constexpr void await_resume() const noexcept {}

        【3】std::suspend_always yield_value (T v) 这个接口会在 co_yield v 时被调用,把 co_yield 后面跟着的值 v 做为参数传入,这里一般就是把这个值保存下来,提供给协程的调用者,返回值一般是std::suspend_always {}。
        【4】void return_value (T v) 这个接口会在 co_return v 时被调用,把 co_return 后面跟着的值 v 作为参数传入,这里一般就是把这个值保存下来,提供给协程调用者。
        【5】void return_void () 如果 co_return 后面没有接任何值,那么就会调用这个接口。return_void 和return_value 只能选择一个实现,否则会报编译错误。
        【6】std::suspend_always final_suspend () 在协程最后退出后调用的接口,如果返回 std::suspend_always 。
        【7】协程结束后,则需要用户自行调用 coroutine_handle 的 destroy 接口来释放协程相关的资源。若协程对应的 handle 就已经为空,不能再调用 destroy 了 (会 coredump)。

        【8】void unhandled_exception () 如果协程内的代码抛出了异常,那么这个接口会被调用。

        std::coroutine_handle<promise_type> 是协程的控制句柄类,也是协程函数返回类型的最重要成员,通过标准库里std::coroutine_handle结构体定义,就可以实现与承诺对象的交互能力。恢复协程、销毁协程实例等都是通过该句柄实现。

        2.5 co_await

        一元运算符 co_await 暂停协程并将控制返回给调用方。它的操作数是一个函数表达式:

co_await 函数表达式   

        函数表达式,即函数,其返回一个类似于std::suspend_always可等待结构体(awaitable),就是需要像std::suspend_always一样为该结构体定义await_ready、await_suspend、await_resume函数:

  • 协程函数resuming_on_new_thread,调用函数表达式fun(switch_to_new_thread),fun返回结果,就是一个等待体(这里是awaitable)。
  • 开始调用 await_ready()。如果它的结果按语境转换成 bool 为 false,那么:暂停协程(以各局部变量和当前暂停点填充其协程状态),然后调用 await_suspend 接口,并将协程的句柄传给这个接口。
  • 如果await_ready 返回 true,那么协程完全不会被挂起,直接会去调用 await_resume () 接口,把这个接口作为 await 的返回值,继续执行协程。
  • 调用 await_suspend(handle),其中 handle 是表示当前协程的协程句柄。这个函数内部可以通过这个句柄观察暂停的协程,而且此函数负责调度它以在某个执行器上恢复,或将其销毁(并返回 false 当做调度) ◦
  • 如果 await_suspend 返回 void,那么立即将控制返回给当前协程的调用方/恢复方(此协程保持暂停),否则如果 await_suspend 返回 bool,那么:
    1. 值为 true 时将控制返回给当前协程的调用方/恢复方
    2. 值为 false 时恢复当前协程。
  • 如果 await_suspend 返回某个其他协程的协程句柄,那么(通过调用 handle.resume())恢复该句柄(注意这可以连锁进行,并最终导致当前协程恢复)
  • 如果 await_suspend 抛出异常,那么捕捉该异常,恢复协程,并立即重抛异常
  • 最后,调用 await_resume(),它的结果就是整个 co_await expr 表达式的结果。
  • 如果协程在 co_await 表达式中暂停而在后来恢复,那么恢复点处于紧接对 await_resume() 的调用之前。

        注意,因为协程在进入 await_suspend() 前已经完全暂停,所以该函数可以自由地在线程间转移协程柄而无需额外同步。例如,可以将它放入回调,将它调度成在异步 I/O 操作完成时在线程池上运行等。此时因为当前协程可能已被恢复,从而执行了等待器的析构函数,同时由于 await_suspend() 在当前线程上持续执行,await_suspend() 应该把 *this 当作已被销毁并且在柄被发布到其他线程后不再访问它。

          2.6 co_await案例

        下面例子,定义了一个awaitable,具有定义await_ready、await_suspend、await_resume函数成员函数,通过switch_to_new_thread函数表达式返回co_await。

//test2.h
#ifndef _TEST_2_H_
#define _TEST_2_H_
void coroutine_wait_test(void);
#endif //_TEST_2_H_
//test2.cpp
#include "test2.h"
#include <coroutine>
#include <iostream>
#include <stdexcept>
#include <thread>
 
auto switch_to_new_thread(std::jthread& out) 
{
    struct awaitable 
    {
        std::jthread* p_out;
        //co_await开始会调用,根据返回值决定是否挂起协程
        bool await_ready() { return false; }
        //在协程挂起后会调用这个,如果返回true,会返回调用者,如果返回false,会立刻resume协程
        void await_suspend(std::coroutine_handle<> h) 
        {
            std::jthread& out = *p_out;
            if (out.joinable())
                throw std::runtime_error("jthread out arg is unull");
            out = std::jthread([h] { h.resume(); });//新建线程,并协程恢复
            std::cout << "new thread ID:" << out.get_id() << "\n"; //
        }
        //在协程resume的时候会调用这个,这个的返回值会作为await的返回值
        void await_resume() {}
    };
    return awaitable{&out};
}
 
struct Task{
    struct promise_type {
        Task get_return_object() { return {}; }
        std::suspend_never initial_suspend() { return {}; }
        std::suspend_never final_suspend() noexcept { return {}; }
        void return_void() {}
        void unhandled_exception() {}
    };
};
 
Task resuming_on_new_thread(std::jthread& out) {
    std::cout << "initial,ID:" << std::this_thread::get_id() << "\n";
    co_await switch_to_new_thread(out);//协程等待
    // 等待器在此销毁
    std::cout << "final,ID:" << std::this_thread::get_id() << "\n";
}

void coroutine_wait_test(void)
{
    std::jthread out;
    auto ret = resuming_on_new_thread(out);
};
//main.cpp
#include "test2.h"

int main(int argc, char* argv[])
{
    coroutine_wait_test();
    return 0;
};

       编译g++ main.cpp test*.cpp -o test.exe -std=c++20及运行程序:

         2.7 无操作协程

        前面描述协程支持库就提到过无操作协程,相比一般协程,它体现如此特征:在协程控制流外不做任何事,在开始和恢复后立即暂停,拥有一种协程状态,而销毁该状态为无操作,若有任何指代它的 std::coroutine_handle 则绝不抵达其最终暂停点。

无操作协程,定义于头文件 <coroutine>
noop_coroutine          (C++20)创建在等待或销毁时无可观察作用的协程柄(函数) 
noop_coroutine_promise  (C++20)用于无可观察作用的协程(类) 
noop_coroutine_handle   (C++20)std::coroutine_handle<std::noop_coroutine_promise> ,有意用于指代无操作协程
(typedef) 

        std::noop_coroutine_promise是是无操作协程的承诺类型,本质上就是一个前面讲述的空promise_type结构体:

//定义于头文件 <coroutine>
struct noop_coroutine_promise {}; 

        而std::noop_coroutine_handle就是std::coroutine_handle句柄以std::noop_coroutine_promise为承诺对象的特例化,

//定义于头文件 <coroutine>,(C++20 起) 
template< class Promise = void > struct coroutine_handle;

template<> struct coroutine_handle<std::noop_coroutine_promise>;
using noop_coroutine_handle = std::coroutine_handle<std::noop_coroutine_promise>;

        std::noop_coroutine是一个函数,用来返回指代无操作协程的协程柄。

/*std::noop_coroutine,定义于头文件 <coroutine>,(C++20 起) 
*返回值指代无操作协程的 std::noop_coroutine_handle 
*若已有无操作协程的协程状态,则不指定 noop_coroutine 的后续调用是返回先前获得的协程柄,
*还是指代新的无操作协程的协程状态的协程柄。
*/
std::noop_coroutine_handle noop_coroutine() noexcept;

        2.8 无操作协程案例

        这是协程函数嵌套的例子,该例子里协程函数test调用了协程函数get_random,它们的返回值都是Task<int>。协程返回类型内,定义了一个可等待体awaiter,在co_await调用时开始触发。另外还为协程返回类型Task定义了承诺类型promise_type,及在该承诺类型内定义了一个可等待体final_awaiter,它会在承诺类型调用final_suspend时构建和开发触发。可等待体final_awaiter的await_suspend函数在传递协程句柄有效时直接返回,以恢复先前的协程;否则返回 noop_coroutine() ,其恢复不做任何事。

//test3.h
#ifndef _TEST_3_H_
#define _TEST_3_H_

void coroutine_noop_test(void);

#endif //_TEST_3_H_
//test3.cpp
#include "test3.h"
#include <coroutine>
#include <utility>
#include <iostream>
 
template<class T>
struct Task {
    struct promise_type {//承诺类型
        promise_type() : result(T()),previous(std::noop_coroutine())
        {
            std::cout << "in Task::promise_type()\n";
        };
        auto get_return_object() {
            std::cout << "in Task::promise_type::get_return_object()\n";
            return Task(std::coroutine_handle<promise_type>::from_promise(*this));
        }
        //返回std::suspend_always{} ,表示await表达式应该始终暂停,不立即执行,先挂起
        std::suspend_always initial_suspend() { 
            std::cout << "in Task::promise_type::initial_suspend()\n";
            return {}; 
        }
        //可等待体定义
        struct final_awaiter {
            //co_await开始会调用,根据返回值决定是否挂起协程
            bool await_ready() noexcept(true) { 
                std::cout << "in Task::promise_type::final_awaiter::await_ready()\n";
                return false; 
            }
            //在协程挂起后会调用这个,如果返回true,会返回调用者,如果返回false,会立刻resume协程
            std::coroutine_handle<> await_suspend(std::coroutine_handle<promise_type> h) noexcept(true)
            {
                // 在当前协程(以 'h' 指代)执行即将结束时调用 final_awaiter::await_suspend 。
                // 若当前协程被另一协程经由 co_await get_Task() 恢复,则存储到该协程的柄
                // 为 h.promise().previous 。该情况下,返回柄以恢复先前的协程。
                // 否则返回 noop_coroutine() ,其恢复不做任何事。
                std::cout << "in Task::promise_type::final_awaiter::await_suspend()\n";
                std::cout << "T = " << h.promise().result << "\n";//co_return *传递的值,不规范语句,主要为了测试逻辑展示
                auto previous = h.promise().previous;
                if (previous) {
                    return previous;
                } else {
                    return std::noop_coroutine();
                }
            }
            //在协程resume的时候会调用这个,这个的返回值会作为final_awaiter的返回值
            void await_resume() noexcept(true) {
                std::cout << "in Task::promise_type::final_awaiter::await_resume()\n";
            }
        };
        //返回final_awaiter{},Task结束协程时,将进入promise_type.final_suspend,进入final_awaiter等待体执行逻辑
        final_awaiter final_suspend() noexcept { 
            std::cout << "in Task::promise_type::final_suspend()\n";
            return {}; 
        }
        void unhandled_exception() { throw; }
        //会在 co_return v 时被调用,把这个v值保存下来,提供给协程调用者
        void return_value(T value) { 
            std::cout << "in Task::promise_type::return_value()\n";
            result += std::move(value); 
        }
        T result;
        std::coroutine_handle<> previous;
    };
    //
    Task(std::coroutine_handle<promise_type> h) : coro(h) {//get_return_object函数内调用
        std::cout << "in Task()\n";
    }
    Task(Task&& t) = delete;
    ~Task() { 
        std::cout << "in ~Task()\n";
        coro.destroy(); 
    }
    //可等待体定义
    struct awaiter {
        //co_await开始会调用,根据返回值决定是否挂起协程
        bool await_ready() { 
            std::cout << "in Task::awaiter::await_ready()\n";
            return false; //挂起
        }
        //在协程挂起后会调用这个,如果返回true,会返回调用者,如果返回false,会立刻resume协程
        auto await_suspend(std::coroutine_handle<> h) {
            std::cout << "in Task::awaiter::await_suspend()\n";
            coro.promise().previous = h;//将Task协程句柄指向的promise,其内部定义std::coroutine_handle<> 特例化句柄
            return coro;
        }
        //在协程resume的时候会调用这个,这个的返回值会作为awaiter的返回值
        T await_resume() { 
            std::cout << "in Task::awaiter::await_resume()\n";
            return std::move(coro.promise().result); 
        }
        std::coroutine_handle<promise_type> coro;
    };
    awaiter operator co_await() { //co_await调用
        std::cout << "in Task::co_await()\n";
        return awaiter{coro}; //将Task协程句柄传入
    }
    T operator()() {
        std::cout << "in Task::operator()\n";
        coro.resume();
        return std::move(coro.promise().result);
    }
private:
    std::coroutine_handle<promise_type> coro;//协程句柄
};

//协程函数,返回Task<int>
Task<int> get_random() {
    std::cout << "in get_random()\n";
    co_return 4;
};

//协程函数,返回Task<int>
Task<int> test() {
    Task<int> v = get_random(); //
    Task<int> u = get_random();
    std::cout << "in test()\n";
    int x = (co_await v + co_await u);//相当于调用Task::co_await()
    co_return x;
};


void coroutine_noop_test(void)
{
    Task<int> t = test();//
    int result = t();
    std::cout << result << '\n';
};
//main.cpp
#include "test3.h"

int main(int argc, char* argv[])
{
    coroutine_noop_test();
    return 0;
};

        编译g++ main.cpp test3.cpp -o test.exe -std=c++20,运行测试:

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Kettle-佛系总结】

Kettle-佛系总结Kettle-佛系总结1.kettle介绍2.kettle安装3.kettle目录介绍4.kettle核心概念1.转换2.步骤3.跳&#xff08;Hop&#xff09;4.元数据5.数据类型6.并行7.作业5.kettle转换1.输入控件1.csv文件输入2.文本文件输入3.Excel输入4.XML输入5.JSON输入6.表输入2.输出控件…

百度Apollo规划算法——轨迹拼接

百度Apollo规划算法——轨迹拼接引言轨迹拼接1、什么是轨迹拼接&#xff1f;2、为什么要进行轨迹拼接&#xff1f;3、结合Apollo代码为例理解轨迹拼接的细节。参考引言 在apollo的规划算法中&#xff0c;在每一帧规划开始时会调用一个轨迹拼接函数&#xff0c;返回一段拼接轨迹…

Kubernetes之服务发布

学了服务发现后&#xff0c;svc的IP只能被集群内部主机及pod才可以访问&#xff0c;要想集群外的主机也可以访问svc&#xff0c;就需要利用到服务发布。 NodePort Nodeport服务是外部访问服务的最基本方式。当我们创建一个服务的时候&#xff0c;把服务的端口映射到kubernete…

【大数据AI人工智能】常见的归一化函数有哪些?分别用数学公式详细介绍

常见的归一化函数有哪些?分别用数学公式详细介绍一下。 常见的归一化函数 常见的归一化函数包括: Min-Max 归一化Z-Score 归一化Log 归一化Sigmoid 归一化下面分别介绍这些归一化函数以及它们的数学公式。 1. Min-Max 归一化 Min-Max 归一化是将原始数据线性映射到 [0,1]…

dp模型——状态机模型C++详解

状态机定义状态机顾名思义跟状态有关系&#xff0c;但到底有什么关系呢。在实际解决的时候&#xff0c;通常把状态想成节点&#xff0c;状态的转换想成有向边的有向图&#xff0c;我们来举个例子。相信大家都玩过类似枪战的游戏&#xff08;没玩过的也听说过吧&#xff09;&…

4.创建和加入通道相关(network.sh脚本createChannel函数分析)[fabric2.2]

fabric的test-network例子有一个orderer组织、两个peer组织、每个组织一个节点&#xff0c;只有系统通道&#xff08;system-channel&#xff09;&#xff0c;没有其他应用通道。我们可以使用./network.sh createChannel命令来创建一个名为mychannel的应用通道。 一、主要概念 …

【Java开发】JUC进阶 04:线程池详解

1 线程池介绍由于频繁创建销毁线程要调用native方法比较消耗资源&#xff0c;为了保证内核的充分利用&#xff0c;所以引入了线程池的概念。&#x1f4cc; 线程池优点降低资源消耗提高响应速度方便管理&#x1f4cc; 创建线程池使用Executors创建使用ThreadPoolExecutor创建&am…

Git图解-为啥是Git?怎么装?

目录 零、学习目标 一、版本控制 1.1 团队开发问题 1.2 版本控制思想 1.2.1 版本工具 二、Git简介 2.1 简介 2.2 Git环境的搭建 三、转视频版 零、学习目标 掌握git的工作流程 熟悉git安装使用 掌握git的基本使用 掌握分支管理 掌握IDEA操作git 掌握使用git远程仓…

【教程】记录Typecho Joe主题升级与Joe魔改版

目录 升级Joe 其他魔改版 Joe主题挺好看的&#xff0c;很早之前我就装了。后来官方升级了主题&#xff0c;但没有给升级教程。这里记录一下我的升级过程&#xff0c;供大家参考。 Joe Github&#xff1a;GitHub - HaoOuBa/Joe: A Theme of Typecho 升级站点&#xff1a;小锋学…

WSL2使用Nvidia-Docker实现CUDA版本自由切换

众所周知&#xff0c;深度学习的环境往往非常麻烦&#xff0c;经常不同的项目所依赖的 torch、tensorflow 包对 CUDA 的版本也有不同的要求&#xff0c;Linux 下进行 CUDA 的管理比较麻烦&#xff0c;是一个比较头疼的问题。 随着 WSL2 对物理机显卡的支持&#xff0c;Nvidia-…

用二极管和电容过滤电源波动,实现简单的稳压 - 小水泵升压改装方案

简而言之&#xff0c;就是类似采样保持电路&#xff0c;当电源电压因为电机启动而骤降时&#xff0c;用二极管避免电容电压跟着降低&#xff0c;从而让电容上连接的低功耗芯片有一个比较稳定的供电电压。没什么特别的用处&#xff0c;省个LDO 吧&#xff0c;电压跌幅太大的时候…

最详细Sql语句优化大汇总 面试必问 含解释

欢迎补充和纠正&#xff01;&#xff01;&#xff01; 目录 欢迎补充和纠正&#xff01;&#xff01;&#xff01; 基础知识 相关索引的创建 一条sql语句的执行过程 sql语句关键字的执行顺序 SQL优化 使用explain来分析Sql语句 尽量用varchar代替char 使用数值代替字符…

Vector - CAPL - 定时器函数和使用

定时器在C语言中的使用我想学习过C编程的都不会陌生&#xff0c;它能够提供延时&#xff0c;完成等待一定的时间&#xff1b;它也可以实现多线程的操作&#xff0c;并行实行某些软件功能。那在CAPL中&#xff0c;定时器又能做哪些工作呢&#xff1f;又是怎么使用的呢&#xff1…

SPringCloud:Nacos快速入门及相关属性配置

目录 一、Nacos快速入门 1、在父工程中添加spring-cloud-alilbaba的管理依赖 2、如果有使用eureka依赖&#xff0c;将其注释 3、添加nacos的客户端依赖 4、修改yml文件&#xff0c;注释eureka配置 5、启动测试 二、Nacos相关属性配置 1、Nacos服务分级存储 2、根据集群…

ELasticsearch基本使用——基础篇

1.初识elasticsearch1.1.了解ES1.1.1.elasticsearch的作用elasticsearch是一款非常强大的开源搜索引擎&#xff0c;具备非常多强大功能&#xff0c;可以帮助我们从海量数据中快速找到需要的内容例如&#xff1a;在GitHub搜索代码在电商网站搜索商品在谷歌搜索答案在打车软件搜索…

Oracle中merge Into的用法

Oracle中merge Into的用法 使用场景 在操作数据库时&#xff0c;数据存在的情况下&#xff0c;进行update操作&#xff1b;不存在的情况下&#xff0c;进行insert操作&#xff1b;在Oracle数据库中&#xff0c;能够使用merge into来实现。 基本语法 merge into table_name …

Go项目的目录结构基本布局

前言 随着项目的代码量在不断地增长&#xff0c;不同的开发人员按自己意愿随意布局和创建目录结构&#xff0c;项目维护性就很差&#xff0c;代码也非常凌乱。良好的目录与文件结构十分重要&#xff0c;尤其是团队合作的时候&#xff0c;良好的目录与文件结构可以减少很多不必要…

HashSet原理

HashSet原理HashSet原理1.概述2.底层代码3.原理图解4.总结4.1: 1.7原理总结4.2: 1.8原理总结HashSet原理 1.概述 ​ HashSet 实现 Set 接口&#xff0c;由哈希表&#xff08;实际上是一个 HashMap 实例&#xff09;支持。它不保证 set 的 迭代顺序&#xff1b;特别是它不保证…

MathType7最新版免费数学公式编辑器

话说我也算是 MathType准资深(DB)用户了,当然自从感觉用DB不好之后,我基本上已经抛弃它了,只是前不久因为个别原因又捡起来用了用,30天试用期间又比较深入的折腾了下,也算是变成半个MathType砖家,coco玛奇朵简单介绍一下这款软件:在很可能看到这儿的你还没有出生的某个年月&…

汇编语言程序设计(三)之汇编程序

系列文章 汇编语言程序设计&#xff08;一&#xff09; 汇编语言程序设计&#xff08;二&#xff09;之寄存器 汇编程序 经过上述课程的学习&#xff0c;我们可以编写一个完整的程序了。这章开始我们将开始编写完整的汇编语言程序&#xff0c;用编译和连接程序将它们连接成可…