C++20协程详解

文章目录

什么是协程

我们在学习编程的过程中,逐渐从单线程,到多线程,再到异步编程和并发处理

这些异步与并发的任务不断增加,导致回调结构会变得复杂,为了提高代码的可读性和可维护性,协程(Coroutine)就被引入了

用来简化整个过程的操作

协程是一种特殊的函数,可以在中途暂停,并在稍后恢复执行

与线程不同,协程不会占用独立的系统资源,而是可以由程序直接控制何时暂停和恢复

因此协程其实就是属于某个具体的线程的,可以理解为一个单独的函数,一个单独的执行流,好处就是没有线程的上下文切换的消耗,协程的调度也是自行实现的,会更加灵活

为什么需要协程

简单理解的话,可以理解为,虽然线程已经能够完成绝大部分的任务了,但是线程的粒度还不够精细

举个例子

在网络中,我们调用read函数读取数据,如果缓冲区没有数据的话,整个线程就会一直阻塞到缓冲区有数据,虽然我们可以采用IO多路复用来避免这个问题,但是这也是一种消耗,而且写起来实在不合直觉

如果能把线程细分为更多的子任务就好了,我们可以主动控制多个子任务进行交替执行

假设read函数是一个单独的子任务,当read函数发现缓冲区没有内容时,则直接"阻塞",让出这个线程的执行权,让线程去执行其他的任务

当可读条件满足时,在去唤醒read的子任务,从上次read"阻塞"的地方继续执行

这里的"阻塞"打了引号,因为并非是真的阻塞,而是相当于按下了暂停键,整个线程仍然是没有阻塞的

这样线程的并发就进一步提高了,这里的子任务也可以理解为是一个函数,我们只需要把当前函数的栈空间和寄存器状态保留即可

什么时候使用协程

主要是IO密集型的任务需要,例如网络请求,爬虫,文件读取,web框架等等

对于计算密集型任务还是别用了,因为本身就不需要大量的线程切换,还增加了协程切换的开销

协程的类别

协程在不同的语言实现也不太一样,Python,Java,Go,C++20都支持协程,大致可以分为两大类

  • 有栈协程(stackful),也就是类似于内核的线程,协程拥有自己的栈空间,不同协程的切换需要切换对应栈的上下文,只是操作系统不会陷入内核态,例如goroutine,libco
  • 无栈协程(stackless),这种就是把上下文会放到公共内存,切换的时候使用状态机来切换,而不用切换上下文,比有栈协程要清凉,C++20,Rust,JavaScript都是这种

这里的有栈和无栈指的是协程之间是否存在调用栈的关系

除此之外还有

  • 对称协程,允许协程之间互相调用,可以直接切换到其他的协程,而不必须返回到调用他的协程,可以写你刚才协程环和协程网
  • 非对称协程,也叫做一对多的协程,每个协程的控制权只能返回给直接调用他的协程,这种调用关系是单向的

C++20的协程

C++20的协程是属于无栈协程,非对称协程,对协程的实现的底层原理实际上是通过关键字展开,模板来实现的

协程的使用

关键字

主要的关键字有三个

  • co_await:暂停协程的执行,等待某个任务的完成
  • co_return:返回结果
  • co_yield:生成一个中间结果,暂停协程

当一个函数中出现了这三个关键字中的任意一个时,这个函数就会被判定为协程了

我们来写一个简单的协程猜数字,先大概看一下这是个什么东西

可以的话请跟着思路一起写,而非直接复制完整代码来看,这里还是比较复杂的

co_wait

框架

我们先搭一个基本的框架

cpp 复制代码
#include <coroutine>
#include <iostream>
using namespace std;

struct CoRet
{
};

struct Input
{
};

CoRet Guess()
{
    Input input;
    int g = co_await input;
    cout << "协程:你猜的是 " << g << endl;
}

int main()
{
    auto ret = Guess(); 
    cout << "主函数:猜一猜" << endl;
    return 0;
}

这里我们定义了两个类,分别是输入和输出,和一个协程Guess,怎么看这是一个协程能,因为其中用来co_await

有些奇怪的点是,Guess没有人恶化的返回语句,但是他可以拥有返回类型,这个返回类型就包含了这个协程的管理句柄,还有其中的一些回调函数

这时候编译器提示我们,他在CoRet这个返回值类型中找不到promise_type这个类

编译器需要我们提供promise_type这个类来对协程进行管理,他会在Guess开始执行是自动初始化promise_type这个类

接下来我们添加上相应的代码

cpp 复制代码
/// @brief 协程函数的返回值类型
struct CoRet
{
    /// @brief 用于控制协程的运行,协程内外数据的交换
    struct promise_type
    {
    };
};
/// 虽然这个函数没有任何的返回语句,但是他仍然可以拥有返回类型
/// 这个返回类型就包含了这个协程的管理句柄,还有一些回调函数
CoRet Guess()
{
    // 在这里编译器会隐含的创建一个对象 当然这里是简化过后的
    // CoRet::promise_type promise;
    
    Input input;
    int g = co_await input; 
    cout << "协程:你猜的是 " << g << endl;
}

然后这里编译器优惠提示我们, struct promise_type 里面少了一些函数,我们一一介绍

cpp 复制代码
        suspend_never initial_suspend()
        {
            return {};
        }
        suspend_never final_suspend() noexcept
        {
            return {};
        }
        /// @brief 异常处理
        void unhandled_exception()
        {

        }

首先是这三个 前两个是当协程调用之前和调用结束之后是否需要暂停等待

如何区别呢,就是用suspend_never和suspend_always这两个提供给我们的类,这两个类是可以被直接co_await的对象

suspend_never 不暂停 继续运行

suspend_always 暂停等待 执行逻辑跳回到调用的地方

unhandled_exception当协程出现异常时的处理函数

还少了一个是

cpp 复制代码
        CoRet get_return_object()
        {
            return
            {coroutine_handle<promise_type>::from_promise(*this)};
        }

这个函数是用于获取 协程暂停时返回的对象的,这个对象可以是任意一个对象

但是更加通用的对象其实是返回这个协程的管理句柄,不然没有办法管理协程了就

这里用的就是coroutine_handle<promise_type>类型作为句柄

需要注意的是,这个句柄是需要作为返回值的成员变量的,因此整个CoRet类就是长这样

cpp 复制代码
/// @brief 协程函数的返回值类型
struct CoRet
{
    /// @brief 用于控制协程的运行,协程内外数据的交换
    struct promise_type
    {
        /// @return 提供了两个可以被 co_await 的对象
        /// suspend_never 不暂停 继续运行
        /// suspend_always 暂停等待 执行逻辑跳回到调用的地方
        suspend_never initial_suspend()
        {
            return {};
        }
        suspend_never final_suspend() noexcept
        {
            return {};
        }
        /// @brief 异常处理
        void unhandled_exception()
        {

        }
        /// @note 这个函数是用于获取协程暂停时返回的对象
        /// 这里其实可以返回的是一个任意对象
        /// 但是没有办法通过协程的返回值来控制协程的进行
        /// 最一般的方式是这样的 利用 promise 对象返回一个控制协程运行的句柄
        CoRet get_return_object()
        {
            return
            {coroutine_handle<promise_type>::from_promise(*this)};
        }

    };
    // 控制协程运行的句柄
    coroutine_handle<promise_type> _h; 
};

补全编译器给我们优化的协程就是这样的

cpp 复制代码
/// @brief 
/// @return 协程函数的返回值类型
/// @note
/// 虽然这个函数没有任何的返回语句,但是他仍然可以拥有返回类型
/// 这个返回类型就包含了这个协程的管理句柄,还有一些回调函数
CoRet Guess()
{
    // 在这里编译器会隐含的创建一个对象 当然这里是简化过后的
    // CoRet::promise_type promise;
    // 利用对象产生一个返回的对象,每一次协程被暂停,返回的就是这个对象
    // CoRet ret = promise.get_return_object();
    // 用创建的对象 co_await initial_suspend 的结果
    // co_await promise.initial_suspend();
    

    Input input;
    int g = co_await input; // 运行到这里时跳转出去
    cout << "协程:你猜的是 " << g << endl;

    // co_await promise.final_suspend();

}

然后编译器又提示我们Input类也少东西了

分别是await_ready,await_suspend,await_resume这三个函数

第一个await_ready表示,当遇到co_await 时 是否需要暂停当前协程并跳转回去 如果不需要暂停跳转则返回true 如果需要暂停等待则返回false

cpp 复制代码
    bool await_ready()
    {
        return false;
    }

第二个await_suspend是,在协程即将要被暂停时可以执行的行为

是这样写的

cpp 复制代码
    void await_suspend(coroutine_handle<CoRet::promise_type> h)
    {
        // h.promise(); 这里协程管理句柄的promise成员函数返回的对象的作用
        // 其实就是获取协程的管理对象
    }

参数是当前协程的管理句柄 在即将暂停之间,我们可以得到管理句柄,并且执行一些操作

这个函数的返回值指的是 在这个协程在暂停之后,代码执行要跳转到的地方

如果是 void 则跳转到调用这个协程的地方

如果是 其他协程的句柄 则跳转到对应协程继续运行

类似于析构函数和构造函数

调用子类的构造函数时,也必定会自动调用父类的构造函数

这里就是 在遇到暂停之后,可以使用void返回到原来调用协程的地方,也可以继续调用其他协程

不同之处就是这里需要显式写出来

第三个是await_resume, 在 co_wait 需要一个返回值的时候 才需要写

这里的返回值类型是可以自己设定的,可以代表不同的含义

cpp 复制代码
    int await_resume()
    {
        return 0;
    }

一阶段完成

这时候编译器也不再提示什么报错信息了

完整代码是这样

cpp 复制代码
#include <coroutine>
#include <iostream>
using namespace std;

/// @brief 协程函数的返回值类型
struct CoRet
{
    /// @brief 用于控制协程的运行,协程内外数据的交换
    struct promise_type
    {
        /// @return 提供了两个可以被 co_await 的对象
        /// suspend_never 不暂停 继续运行
        /// suspend_always 暂停等待 执行逻辑跳回到调用的地方
        suspend_never initial_suspend()
        {
            return {};
        }
        suspend_never final_suspend() noexcept
        {
            return {};
        }
        /// @brief 异常处理
        void unhandled_exception()
        {

        }
        /// @note 这个函数是用于获取协程暂停时返回的对象
        /// 这里其实可以返回的是一个任意对象
        /// 但是没有办法通过协程的返回值来控制协程的进行
        /// 最一般的方式是这样的 利用 promise 对象返回一个控制协程运行的句柄
        CoRet get_return_object()
        {
            return
            {coroutine_handle<promise_type>::from_promise(*this)};
        }

    };
    // 控制协程运行的句柄
    //  _h.resume() -> 继续运行协程, _h() -> 也是的
    coroutine_handle<promise_type> _h; 
};

struct Input
{
    /// @brief 当遇到 co_await 时 是否需要暂停当前协程并跳转回去
    /// @return 不需要暂停跳转返回true | 需要暂停等待返回false
    bool await_ready()
    {
        return false;
    }

    /// @brief 定义在协程即将要被暂停时的一些行为
    /// @param h 当前协程的管理句柄
    /// @note 在即将暂停之间,我们可以得到管理句柄,并且执行一些操作
    /// @return 这个函数的返回值指的是 在这个协程在暂停之后,代码执行要跳转到的地方
    /// 如果是 void 则跳转到调用这个协程的地方
    /// 如果是 其他协程的句柄 则跳转到对应协程继续运行
    /// 类似于析构函数和构造函数 
    /// 调用子类的构造函数时,也必定会自动调用父类的构造函数
    /// 这里就是 在遇到暂停之后,可以使用void返回到原来调用协程的地方,也可以继续调用其他协程
    /// 不同之处就是这里需要显式写出来
    void await_suspend(coroutine_handle<CoRet::promise_type> h)
    {
        // h.promise(); 这里协程管理句柄的promise成员函数返回的对象的作用
        // 其实就是获取协程的管理对象
    }

    /// @brief 在 co_wait 需要一个返回值的时候 才需要写
    /// @return 这里的返回值类型是可以自己设定的,可以代表不同的含义
    int await_resume()
    {
        return 0;
    }
};

/// @brief 
/// @return 协程函数的返回值类型
/// @note
/// 虽然这个函数没有任何的返回语句,但是他仍然可以拥有返回类型
/// 这个返回类型就包含了这个协程的管理句柄,还有一些回调函数
CoRet Guess()
{
    // 在这里编译器会隐含的创建一个对象 当然这里是简化过后的
    // CoRet::promise_type promise;
    // 利用对象产生一个返回的对象,每一次协程被暂停,返回的就是这个对象
    // CoRet ret = promise.get_return_object();
    // 用创建的对象 co_await initial_suspend 的结果
    // co_await promise.initial_suspend();
    

    Input input;
    int g = co_await input; // 运行到这里时跳转出去
    // co_await 后面可以加的是一个对象 但是也需要加一点东西
    cout << "协程:你猜的是 " << g << endl;

    // co_await promise.final_suspend();

}

int main()
{
    auto ret = Guess(); // 运行到 co_await 时跳转回来
    cout << "主函数:猜一猜" << endl;
    ret._h.resume(); // 运行到这里时跳转回去
    return 0;
}

运行结果是这样的

我们可以画一下协程和主函数的调用关系

数据交换

这里我们可以发现,在CoRet这对象里的promise对象就是沟通协程内外的桥梁

一个自然的想要给协程输入的方法其实就是给这个promise_type对象中新建一个成员变量,然后在Input对象里面存一个promise_type新增的成员变量的地址

在调用await_suspend()时就使用promise对象中的地址对Input对象中的指针赋值,然后再在await_resume()中返回即可

显然这种方法曲折又复杂,没什么人喜欢的

这样我们创建一个新的类 来作为输入,将这个类作为协程的成员变量

这个类在main中初始化,然后通过参数传递给函数,再用协程的Input接收这个参数并初始化协程,就能够完成协程和main函数之间的参数传递了

cpp 复制代码
struct Note
{
    int num;
    Note() : num(100) {}
};
struct Input
{
    Note &in;
    bool await_ready()
    {
        cout << "await_ready" << endl;
        return false;
    }
    void await_suspend(coroutine_handle<CoRet::promise_type> h)
    {
        cout << "await_suspend" << endl;
    }

    int await_resume()
    {
        cout << "await_resume" << endl;
        return in.num;
    }
};
CoRet Guess(Note &note)
{
    Input input{note};
    int g = co_await input; 
    cout << "协程:你猜的是 " << g << endl;
}
int main()
{
    Note note;
    auto ret = Guess(note); 
    cout << "主函数:猜一猜" << endl;
    ret._h.resume();
    return 0;
}

co_yield

这个关键字其实就等价于 co_await promise.yield_value(expr)

这里我们再加一点东西

在Guess开始的时候,产生一个1到114的随机数 int res = (rand() % 114 + 1);

然后从协程外面读取一个数字

再调用co_yield对比较数字的大小

co_yield (res > g ? 1 : (res == g ? 0 : -1));

这里三目运算符的结果无非就是-1 0 1这三种

就相当于调用了这样的函数 co_await promise.yield_value(-1\0\1);

但是我们并没有实现yield_value 因此需要写一下 他的输入其实就是co_yield后面的那个表达式 返回值则是用于区分执行完之后是否需要暂停等待 如果是suspend_never则不等待 如果是suspend_always则进行等待

为了保存这个co_yield输入的参数,我们需要在promise对象中添加一个参数

cpp 复制代码
        suspend_always yield_value(int r)
        {
            out = r;
            return {};
        }
        int out;

这样当协程比完大小之后,就会把结果赋值到协程管理句柄的out中

然后在主函数中就可以直接取到结果了

cpp 复制代码
int main()
{
    srand(time(nullptr));
    Note note;
    auto ret = Guess(note); // 运行到 co_await 时跳转回来
    cout << "主函数:猜一猜" << endl;
    note.num = 10;
    ret._h.resume(); // 运行到这里时跳转回去
    cout << "主函数:猜的结果是:" << ((ret._h.promise().out == 1) ? "猜大了" : ((ret._h.promise().out == 0) ? "猜对了" : "猜小了"))
         << endl;
    return 0;
}

co_return

这个co_return其实和co_yield是差不多的

他也是会调用一个return_value的函数,不同的是,他会在管理句柄中标记这个协程是否已经完成

在协程管理句柄中有一个成员函数是done(),可以获取协程是否执行完成的状态

在这个例子里面,我们想要获取res,同样就是在promise对象中添加一个参数 然后进行赋值就行

cpp 复制代码
        void return_value(int r)
        {
            res = r;
        }
        int res;

在Guess的最后调用co_return res;就会自动转化为co_await return_value(res);

然后在main函数中就可以读取到了

完整代码

cpp 复制代码
#include <coroutine>
#include <iostream>
using namespace std;

/// @brief 协程函数的返回值类型
struct CoRet
{
    /// @brief 用于控制协程的运行,协程内外数据的交换
    struct promise_type
    {
        /// @return 提供了两个可以被 co_await 的对象
        /// suspend_never 不暂停 继续运行
        /// suspend_always 暂停等待 执行逻辑跳回到调用的地方
        suspend_never initial_suspend()
        {
            cout << "initial_suspend" << endl;

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

            return {};
        }
        /// @brief 异常处理
        void unhandled_exception()
        {
        }
        /// @note 这个函数是用于获取协程暂停时返回的对象
        /// 这里其实可以返回的是一个任意对象
        /// 但是没有办法通过协程的返回值来控制协程的进行
        /// 最一般的方式是这样的 利用 promise 对象返回一个控制协程运行的句柄
        CoRet get_return_object()
        {
            cout << "get_return_object" << endl;

            return {coroutine_handle<promise_type>::from_promise(*this)};
        }
        suspend_always yield_value(int r)
        {
            out = r;
            return {};
        }
        int out;

        void return_value(int r)
        {
            res = r;
        }
        int res;
    };
    // 控制协程运行的句柄
    //  _h.resume() -> 继续运行协程, _h() -> 也是的
    coroutine_handle<promise_type> _h;
};

struct Note
{
    int num;
};

struct Input
{
    Note &in;
    /// @brief 当遇到 co_await 时 是否需要暂停当前协程并跳转回去
    /// @return 不需要暂停跳转返回true | 需要暂停等待返回false
    bool await_ready()
    {
        cout << "await_ready" << endl;
        return false;
    }

    /// @brief 定义在协程即将要被暂停时的一些行为
    /// @param h 当前协程的管理句柄
    /// @note 在即将暂停之间,我们可以得到管理句柄,并且执行一些操作
    /// @return 这个函数的返回值指的是 在这个协程在暂停之后,代码执行要跳转到的地方
    /// 如果是 void 则跳转到调用这个协程的地方
    /// 如果是 其他协程的句柄 则跳转到对应协程继续运行
    /// 类似于析构函数和构造函数
    /// 调用子类的构造函数时,也必定会自动调用父类的构造函数
    /// 这里就是 在遇到暂停之后,可以使用void返回到原来调用协程的地方,也可以继续调用其他协程
    /// 不同之处就是这里需要显式写出来
    void await_suspend(coroutine_handle<CoRet::promise_type> h)
    {
        cout << "await_suspend" << endl;
        // h.promise(); 这里协程管理句柄的promise成员函数返回的对象的作用
        // 其实就是获取协程的管理对象
    }

    /// @brief 在 co_wait 需要一个返回值的时候 才需要写
    /// @return 这里的返回值类型是可以自己设定的,可以代表不同的含义
    int await_resume()
    {
        cout << "await_resume" << endl;
        return in.num;
    }

};

/// @brief
/// @return 协程函数的返回值类型
/// @note
/// 虽然这个函数没有任何的返回语句,但是他仍然可以拥有返回类型
/// 这个返回类型就包含了这个协程的管理句柄,还有一些回调函数
CoRet Guess(Note &note)
{
    // 在这里编译器会隐含的创建一个对象 当然这里是简化过后的
    // CoRet::promise_type promise;
    // 利用对象产生一个返回的对象,每一次协程被暂停,返回的就是这个对象
    // CoRet ret = promise.get_return_object();
    // 用创建的对象 co_await initial_suspend 的结果
    // co_await promise.initial_suspend();
    int res = (rand() % 114) + 1;
    Input input{note};
    int g = co_await input; // 运行到这里时跳转出去
    // co_await 后面可以加的是一个对象 但是也需要加一点东西
    cout << "协程:你猜的是 " << g << endl;
    co_yield (res > g ? 1 : (res == g ? 0 : -1));
    co_return res;
    // co_await promise.final_suspend();
}

int main()
{
    srand(time(nullptr));
    Note note;
    auto ret = Guess(note); // 运行到 co_await 时跳转回来
    cout << "主函数:猜一猜" << endl;
    note.num = 10;
    ret._h.resume(); // 运行到这里时跳转回去
    cout << "主函数:猜的结果是:" << ((ret._h.promise().out == 1) ? "猜大了" : ((ret._h.promise().out == 0) ? "猜对了" : "猜小了"))
         << endl;
    cout << "主函数:随机数是:" << ret._h.promise().res << endl;
    return 0;
}
相关推荐
xq51486313 分钟前
内存分配与回收策略
java·开发语言·jvm
叫我阿呆就好了27 分钟前
C 语言复习总结记录二
c语言·开发语言
cuisidong199729 分钟前
springmvc 用了 @RequestMapping 是不是可以不用
java·开发语言
《源码好优多》30 分钟前
基于Java Springboot公园管理系统
java·开发语言·spring boot
我救我自己30 分钟前
UE5 slate BlankProgram独立程序系列
java·开发语言·ue5
泰山小张只吃荷园1 小时前
SCAU软件体系结构实验四 组合模式
java·开发语言·组合模式
无休居士1 小时前
流式上传与分片上传的原理与实现
开发语言·php·分片上传·断点续传
夫琅禾费米线2 小时前
[有趣的JavaScript] 为什么typeof null返回 object
开发语言·前端·javascript
三掌柜6662 小时前
【腾讯云产品最佳实践】腾讯云CVM入门技术与实践:通过腾讯云快速构建云上应用
开发语言·腾讯云·perl
多来米19962 小时前
小白学多线程(持续更新中)
java·开发语言