C++20 特性 协程 Coroutines(1)

目录
  • 一、协程简单介绍
  • 二、协程的好处
  • 三、协程得用法
  • 四、协程三个关键字
  • 五、协程工作原理
    • 1、co_yield
    • 2、co_return

我们先来介绍一下什么是协程.

一、协程简单介绍

协程和普通的函数 其实差不多. 不过这个 "函数" 能够暂停自己, 也能够被别人恢复.

普通的函数调用, 函数运行完返回一个值, 结束.

协程可以运行到一半, 返回一个值, 并且保留上下文. 下次恢复的时候还可以接着运行, 上下文 (比如局部变量) 都还在.

这就是最大的区别.

二、协程的好处

考虑多任务协作的场景. 如果是线程的并发, 那么大家需要抢 CPU 用, 还需要条件变量/信号量或者上锁等技术, 来确保正确的线程正在工作.

如果在协程中, 大家就可以主动暂停自己, 多个任务互相协作. 这样可能就比大家一起抢 CPU 更高效一点, 因为你能够控制哪个协程用上 CPU.

一个例子:

生产者/消费者模型: 生产者生产完毕后, 暂停自己, 把控制流还给消费者. 消费者消费完毕后, resume 生产者, 生产者继续生产. 这样循环往复.

异步调用: 比如你要请求网络上的一个资源.

  • 发请求给协程
  • 协程收到请求以后, 发出请求. 协程暂停自己, 把控制权还回去.
  • 你继续做些别的事情. 比如发出下一个请求. 或者做一些计算.
  • 恢复这个协程, 拿到资源 (可能还要再等一等)

理想状态下, 4 可以直接用上资源, 这样就完全不浪费时间.

如果是同步的话:

  • 发请求给函数.
  • 函数收到请求以后, 等资源.
  • 等了很久, 资源到了, 把控制权还回去.

明显需要多等待一会儿. 如果需要发送上百个请求, 那显然是第一种异步调用快一点. (等待的过程中可以发送新的请求)

如果没有协程的话, 解决方案之一是使用多线程. 像这样:

  • 发请求给函数.
  • 函数在另外的线程等, 不阻塞你的线程.
  • 你继续做些别的事情. 比如发出下一个请求. 或者做一些计算.
  • 等到终于等到了, 他再想一些办法通知你.

然后通知的办法就有 promise 和回调这些办法.

三、协程得用法

我们照着 C++20 标准来看看怎么用协程. 用 g++, 版本 10.2 进行测试.

目前 C++20 标准只加入了协程的基本功能, 还没有直接能上手用的类. GCC 说会尽量与 clang MSVC 保持协程的 ABI 兼容, 同时和 libc++ 等保持库的兼容. 所以本文可能也适用于它们.

协程和主程序之间通过 promise 进行通信. promise 可以理解成一个管道, 协程和其调用方都能看得到.

以前的 std::async std::future 也是基于一种特殊的 promise 进行通信的, 就是 std::promise. 如果要使用协程, 则需要自己实现一个全新的 promise 类, 原理上是类似的.

四、协程三个关键字

这次引入了三个新的关键字 co_await, co_yield, co_return . 从效果上看: co_await 是用来暂停和恢复协程的, 并且真正用来求值.

co_yield 是用来暂停协程并且往绑定的 promise 里面 yield 一个值.

co_return 是往绑定的 promise 里面放入一个值.

这里我们先谈谈 co_yield co_return. 谈完这俩再谈谈 co_await 就比较简单.

五、协程工作原理

所以最重要的两个问题就是

  • 协程如何实现信息传递 (使用自己实现的 promise)
  • 如何恢复一个已经暂停了的协程 (使用 std::coroutine_handle)

上面说了, 一个协程会有一个与之相伴的 promise , 用作信息传递. 一个协程, 效果等同于

{
promise-type promise(promise-constructor-arguments);
try {
    co_await promise.initial_suspend(); // 创建之后 第一次暂停
    function-body // 函数体
} catch ( ... ) {
    if (!initial-await-resume-called)
    throw;
    promise.unhandled_exception();
}

final-suspend:
co_await promise.final_suspend(); // 最后一次暂停
}

细节, 包括 promise 初始化的参数, 异常的处理等等, 我们留到之后的文章再处理. 所以我们简化成

{
promise-type promise; 

co_await promise.initial_suspend(); 

function-body // 函数体

final-suspend:
co_await promise.final_suspend();
}

对于暂停, co_await 那个地方就可以暂停并且交出控制权. 下篇文章我们会详细介绍 co_await.

对于唤醒, 则需要拿到一个 std::coroutine_handle, 对它调用 resume() .

1、co_yield

co_yield 123 做的事情实际上相当于调用了 co_await promise.yield_value(123) . 这个 promise 里面存放了 123 以后, 会告诉 co_await 自己要暂停. 于是 co_await 就在这里停下来, 把控制流还回去.

来看一个标准中的实现范例.

#include <iostream>
#include <coroutine>

struct generator
{
    struct promise_type;
    using handle = std::coroutine_handle<promise_type>;
    struct promise_type
    {
        int current_value;
        static auto get_return_object_on_allocation_failure() { return generator{nullptr}; }
        auto get_return_object() { return generator{handle::from_promise(*this)}; }
        auto initial_suspend() { return std::suspend_always{}; }
        auto final_suspend() { return std::suspend_always{}; }
        void unhandled_exception() { std::terminate(); }
        void return_void() {}
        auto yield_value(int value)
        {
            current_value = value;
            return std::suspend_always{}; // 这是一个 awaiter 结构, 见第二篇文章
        }
    };
    bool move_next() { return coro ? (coro.resume(), !coro.done()) : false; }
    int current_value() { return coro.promise().current_value; }
    generator(generator const &) = delete;
    generator(generator &&rhs) : coro(rhs.coro) { rhs.coro = nullptr; }
    ~generator() {  if (coro) coro.destroy();   }

private:
    generator(handle h) : coro(h) {}
    handle coro;
};

generator f()
{
    co_yield 1;
    co_yield 2;
}

int main()
{
    auto g = f(); // 停在 initial_suspend 那里
    while (g.move_next()) // 每次调用就停在下一个 co_await 那里
        std::cout << g.current_value() << std::endl;
}

generator 是一个包装类, 持有一个 std::coroutine_handle. 同时它规定了 coroutine_handle 本协程的 promise 是什么样的. (通过 generator::promise_type告知)

coroutine_handle是协程的流程管理者, 由它来管理这个 promise. 而 generator 则是 coroutine_handle 的管理者.

f() 是一个协程. 可以展开成这样的伪代码

{
generator g(handle coro); // 建立句柄和包装类

co_await promise.initial_suspend(); // 创建之后停在这里, 等待被恢复

co_await promise.yield_value(1); // 第一次恢复后就会停在这里
co_await promise.yield_value(2); // 第二次恢复后就会停在这里

final-suspend:
co_await promise.final_suspend(); // 第三次恢复后就会停在这里
}

按照这里的写法, 每一次 promise.yield_value() 之后都会返回一个结构体给 co_await, 告诉 co_await 自己在这里暂停.

然后在主函数处调用 g.move_next() , 进而恢复了协程之后, 协程就会从刚刚暂停的 co_await 那一行恢复运行.

对了, 过了最后的 final_suspend() 以后, 这个协程就会析构掉. 再次恢复协程就会导致 segmentation fault.

g++10 已经提供了协程的支持, 只需要加上 -std=c++20 -fcoroutines -fno-exceptions 即可. 上面这段代码可以在这里编译:

2、co_return

co_return 相当于调用了 promise.return_value() 或者 promise.return_void() 然后跳到 final-suspend 标签那里. 也就是说这个这个协程结束了, 再也无法被恢复了.

而对比 co_yield 调用的是 co_await promise.yield_value(). 他们的区别就是 co_yeild 完了协程继续等着下一次被恢复 , co_return co_return完了协程就结束了. (为了让协程也能像普通函数一样返回)

我们来看一段代码.

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

using namespace std;

struct lazy
{
    struct promise_type;
    using handle = std::coroutine_handle<promise_type>;
    struct promise_type
    {
        int _return_value;
        static auto get_return_object_on_allocation_failure() { return lazy{nullptr}; }
        auto get_return_object() { return lazy{handle::from_promise(*this)}; }
        auto initial_suspend() { return std::suspend_always{}; }
        auto final_suspend() { return std::suspend_always{}; }
        void unhandled_exception() { std::terminate(); }
        void return_value(int value) { _return_value = value; }
    };
    bool calculate()
    {
        if (calculated)
            return true;
        if (!coro)
            return false;
        coro.resume();
        if (coro.done())
            calculated = true;
        return calculated;
    }
    int get() { return coro.promise()._return_value; }
    lazy(lazy const &) = delete;
    lazy(lazy &&rhs) : coro(rhs.coro) { rhs.coro = nullptr; }
    ~lazy() {  if (coro) coro.destroy(); }

private:
    lazy(handle h) : coro(h) {}
    handle coro;
    bool calculated{false};
};

lazy f(int n = 0)
{
    co_return n + 1;
}

int main()
{
    auto g = f();
    g.calculate(); // 这时才从 initial_suspend 之中恢复, 所以就叫 lazy 了
    cout << g.get();
}

由于这个协程只能被恢复一次, 所以我稍稍修改了一下 lazy 的实现. 可以参考这里:

下一篇C++20 新特性 协程 Coroutines(2)

到此这篇关于C++20 特性 协程 Coroutines的文章就介绍到这了,更多相关C++20 协程 Coroutines内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • C++20中的协程(Coroutine)的实现

    C++20中的协程(Coroutine) 从2017年开始, 协程(Coroutine)的概念就开始被建议加入C++20的标准中了,并已经开始有人对C++20协程的提案进行了介绍.1事实上,协程的概念在很早就出现了,甚至其他语言(JS,Python,C#等)早就已经支持了协程. 可见,协程并不是C++所特有的概念. 那么,什么是协程? 简单来说,协程就是一种特殊的函数,它可以在函数执行到某个地方的时候暂停执行,返回给调用者或恢复者(可以有一个返回值),并允许随后从暂停的地方恢复继续执行.注意,这

  • C++20 新特性 协程 Coroutines(2)

    目录 1.co_await 2.awaiter 的三个接口用途 3.协程用法的回顾 想了解上一篇文章内容的小伙伴可点击 C++20 特性 协程 Coroutines (1) 谈到什么是协程. 并且介绍了 co_yield 和 co_return 的作用. 这篇来介绍一下 co_await. 1.co_await 一个形如: co_await awaitable 的表达式就叫一个 await-expression. co_await 表达式是用来暂停当前协程的运行, 转而等待 awaitable

  • 详解c++20协程如何使用

    什么是协程 新接触的人看了网上很多人的见解都是一头雾水,本人的理解,协程就是可中断的函数,这个函数在执行到某一时刻可以暂停,保存当前的上下文(比如当前作用域的变量,函数参数等等),在后来某一时刻可以手动恢复这个中断的函数,把保存的上下文恢复并从中断的地方继续执行.简而言之,协程就是可中断的函数,协程如何实现:保存上下文和恢复上下文. 你可能会说协程不会这么简单的吧,我这里来举例一下啊,如python的协程 def test(): print('begin') yield print('hello

  • C++20 特性 协程 Coroutines(1)

    目录 一.协程简单介绍 二.协程的好处 三.协程得用法 四.协程三个关键字 五.协程工作原理 1.co_yield 2.co_return 我们先来介绍一下什么是协程. 一.协程简单介绍 协程和普通的函数 其实差不多. 不过这个 "函数" 能够暂停自己, 也能够被别人恢复. 普通的函数调用, 函数运行完返回一个值, 结束. 协程可以运行到一半, 返回一个值, 并且保留上下文. 下次恢复的时候还可以接着运行, 上下文 (比如局部变量) 都还在. 这就是最大的区别. 二.协程的好处 考虑多

  • C++通信新特性协程详细介绍

    目录 一.关于协程 二.协程的好处 三.协程得用法 四.与线程的区别 五.协程示例 一.关于协程 从 1.54.0 版本开始,Boost.Asio 支持协程.虽然您可以直接使用 Boost.Coroutine,但 Boost.Asio 中对协程的显式支持使得使用它们变得更加容易. 协程让您创建一个反映实际程序逻辑的结构.异步操作不会拆分函数,因为没有处理程序来定义异步操作完成时应该发生什么.程序可以使用顺序结构,而不是让处理程序相互调用. 二.协程的好处 考虑多任务协作的场景. 如果是线程的并发

  • 从使用角度解读c++20 协程示例

    目录 协程长什么样子 c++20的协程三板斧 co_return co_yield co_await 理解协程 协程长什么样子 网上一堆乱七八糟的定义,看的人云里雾里,毫无意义.下面从实战角度看看协程到底长什么样子. 首先,类比线程,线程是个函数.把这个函数交给 创建线程的api,然后这个函数就变成线程了.这个函数本身没有任何特殊的地方,就是普通函数. 相比于线程,协程也是个函数,不过协程函数比线程函数讲究多了. 它必须要有返回值,返回值的类型 还必须’内嵌’一个promise_type类型pr

  • golang协程池设计详解

    Why Pool go自从出生就身带"高并发"的标签,其并发编程就是由groutine实现的,因其消耗资源低,性能高效,开发成本低的特性而被广泛应用到各种场景,例如服务端开发中使用的HTTP服务,在golang net/http包中,每一个被监听到的tcp链接都是由一个groutine去完成处理其上下文的,由此使得其拥有极其优秀的并发量吞吐量 for { // 监听tcp rw, e := l.Accept() if e != nil { ....... } tempDelay = 0

  • 深入理解 Java、Kotlin、Go 的线程和协程

    前言 Go 语言比 Java 语言性能优越的一个原因,就是轻量级线程Goroutines(协程Coroutine).本篇文章深入分析下 Java 的线程和 Go 的协程. 协程是什么 协程并不是 Go 提出来的新概念,其他的一些编程语言,例如:Go.Python 等都可以在语言层面上实现协程,甚至是 Java,也可以通过使用扩展库来间接地支持协程. 当在网上搜索协程时,我们会看到: Kotlin 官方文档说「本质上,协程是轻量级的线程」. 很多博客提到「不需要从用户态切换到内核态」.「是协作式的

  • 深入浅析python中的多进程、多线程、协程

    进程与线程的历史 我们都知道计算机是由硬件和软件组成的.硬件中的CPU是计算机的核心,它承担计算机的所有任务. 操作系统是运行在硬件之上的软件,是计算机的管理者,它负责资源的管理和分配.任务的调度. 程序是运行在系统上的具有某种功能的软件,比如说浏览器,音乐播放器等. 每次执行程序的时候,都会完成一定的功能,比如说浏览器帮我们打开网页,为了保证其独立性,就需要一个专门的管理和控制执行程序的数据结构--进程控制块. 进程就是一个程序在一个数据集上的一次动态执行过程. 进程一般由程序.数据集.进程控

  • 利用Kotlin的协程实现简单的异步加载详解

    前言 众所周知在android中当执行程序的耗时超过5秒时就会引发ANR而导致程序崩溃.由于UI的更新操作是在UI主线程进行的,理想状态下每秒展示60帧时人眼感受不到卡顿,1000ms/60帧,即每帧绘制时间不应超过16.67ms.如果某项操作的耗时超过这一数值就会导致UI卡顿.因此在实际的开发中我通常把耗时操作放在一个新的线程中(比如从网络获取数据,从SD卡读取图片等操作),但是呢在android中UI的更新只能在UI主线程中进行更新,因此当我们在非UI线程中执行某些操作的时候想要更新UI就需

随机推荐