Introduction

自 C++11 开始,很多更新都集中在并发支持上,从最初的线程基础支持,到如今的协程,C++ 已经日趋完善。

协程是继线程之后的又一利器,若论年龄,协程倒比线程要大。由于早期的线程主要在单 CPU 上运行,仅是模拟多线程,故又称“伪线程”。伪线程和协程的思路颇有相似之处,以致难以旌别,其实它们各有应用场景。

本篇将回答下列问题:

  • 什么是协程?
  • 协程与进程和线程有何区别?
  • 什么是并发,和并行有何区别?
  • 协程和函数有何区别?
  • 什么是有栈协程,无栈协程?
  • 协程的使用场景有哪些?
  • Coroutines TS 是什么?
  • 如何使用 C++20 Coroutines?

本篇旨在明析概念,不会出现太多代码,概念清晰后,下篇将有大量代码来解析协程。

Concurrency versus Parallelism

要理清协程的定位,须得先能够旌别并发与并行。可以先来看看百科的定义:

并行是指“并排行走”或“同时实行或实施”。
在操作系统中是指,一组程序按独立异步的速度执行,无论从微观还是宏观,程序都是一起执行的。
对比地,并发是指:在同一个时间段内,两个或多个程序执行,有时间上的重叠(宏观上是同时,微观上仍是顺序执行)。

这里用了两个词:宏观和微观。宏观指从大的角度来看,微观指从小的角度来说。通俗地说,宏观指的是能够通过眼睛看到的,微观则是眼睛看不到的。并发在宏观上同时,微观上为顺序执行。这就是伪线程的实现思路,通过迅速地在各个模块之间切换执行,以迷惑视觉,使得看起来像是同时执行的,其实底层依旧是顺序执行的。

线程在早期并非必须,因为都是命令窗口,就是从上往下依次执行的。而 Windows 拥有界面,在执行任务之时可以随意进行别的操作,因此界面常常会卡死。MS 便搞了个模拟同步运行的机制,便是线程。了解过 CPU 发展历史的会知道,早期 CPU 频率步步直升,人们都以为这种趋势能够持续下去。这也意味着你写的程序不用更新也能随着 CPU 的增强而自动增强性能。

所以早期都是单核 CPU。随着物理瓶颈到来,频率提升之路步步维艰,生产厂商便转变策略,通过将多个核心置于一起,产生多核CPU来提升性能。这便是性能不够,数量来凑。也因此,真正的多线程得以实现。

真线程的执行便为并行执行。由于每个核心都是独立的,因此可以同时执行。此时,无论是视觉上,还是底层实现上,都是真正的同时执行。

既然有并发与并行两种形式,那么我们对其进行排列组合,便能得出4种结果。

Concurrent-Parallel-Matrix

一个既无并行,也无并发的程序处于并发的能力最弱,只能顺序执行每条任务,这一般是一个很小的程序。当有并发,无并行时,能够充分发挥单核CPU的性能,伪线程与协程便属此列。而无并发,有并行时,也就是说是多核CPU,但一个核心上只开了一个线程,此时的确是并发。但和有并发无并行时所达到的效果是一样的,真线程便属此列。

也正因如此,线程和协程的效果只用眼睛是无法分辨的。有很多人便说有了多线程为啥还要加入协程呢?其实协程和线程分工并不同,一个是并发,一个是并行,不过伪线程也属于并发罢了。

那现在你可能又要改变问题了,既然伪线程也是并行,那么请问协程和伪线程又如何区分?

这个问题才是关键,其实主要区别是调度问题,本文第四节将会详细对比协程与线程。

只有并发,或只有并行,都无法处理高并发需求,所以二者呈互补之势,共同发挥CPU的最大性能。

下面再以一个例子来谈谈并发与并行。

假设有一个迷宫。

Maze

现欲寻得迷宫出口,便有四种方式。

在无并发无并行的情况下,只能暴力式的遍历每条路径。因此,在发现一条路不通时,需要原路返回到分岔路口,以接着尝试另一条路径。这意味着很多条路径都要走两次,效率可想而知。

在无并发有并行的情况下,便意味着有多人在同时寻找出口,即使其中一个人走的是死角,其他人亦可继续自己的任务。如此一来,每条路便只需走一遍。当然,这也意味着所耗费的资源也更多,此处对应的便是人力,在计算机中,对应的便是 CPU 核心数。核心数无法持续增加,所以此处便有些是理想状态了,实际情况可能得一人负责多条路径。也就是说一个核上分成多个线程,此时便是真伪线程混合,并发与并行混合了。

那么只有并发是什么情况呢?这时的情况是这样的:一个人先走一部分,接着其他人再走,因为是并行执行,所以这些人无法同时行走。其中一个人走一会儿,停下来,其他人才能接着走。

这样的好处是什么呢?

即使一个人走入死胡同,也无需再原路返回。简单地说,当每个人停止行走时,都需要转到其他人那里去执行。他怎么瞬间跑到其他人那里去呢?其实每个人在跳转之前,都会在原地插一个眼做标记,当需要跳转时,便可直接传送过去。所以在一个人步入死角时,便会放弃这条路,传送到其他人那里继续执行。最后留下的一个人,便是正确的迷宫出口。和并行不同,并发所需的资源要少,可以开启上亿个协程行动,他们之间互相协作,无论多大的迷宫,也能很快找到出口。

最后,并行与并发结合,无疑会指数级提升性能,并发说到底同时还是只有一个人能行动,而并发能同时多人行动,两者互补,才能发挥出最大效率。

Quantum computation

上述所说的统一称为经典计算,既然谈到了并行,便不得不再说下量子计算。

通过上面介绍,可知经典计算要增加计算能力,先是尝试提高处理器频率,无奈达到了物理瓶颈。于是便增加物理资源,用数量来凑。然而,这样的方法来增加计算能力同样是不可持续的,而且增加核心要消耗更多的物理资源。

第一个分岔路口需要2个人,第二个便要4个人,接着便要8个,16个,32个…… 这个数字是呈指数增长的,要不了多久,便会成为天文数字。这也是现如今许多密码学加密技术的依赖,通常数字都会非常大,计算机跑几十年可能都跑不出来。这就是经典计算的瓶颈,理论上算力不可达。

经典计算的算力只能线性增加,而问题规模却是指数增长的,所以经典计算根本追不上问题增长的速度。而量子计算则不同,随着量子比特的增大,其信息容量也会呈指数增长。

像上面的迷宫,若是由量子计算来遍历会如何做呢?

比如其有 10 个人,那么便能同时分出 1024 个分身,这样所有的路径都能被搜索到,只需走一次,就能找出迷宫出口。这被称为量子叠加态的并行演化。同时,量子还有另一个特性——「纠缠性」,可以解决在开发并行程序时需要格外观注的数据竞争问题,通常都是通过锁或原子量来同步数据。

量子纠缠特性使量子计算天生就带有同步性,一个数据被改变了,不论别处还有多少个数据,瞬间全部都会改变。比如有一个分身死了,那么1024个分身便全死了。

所以面对暴力计算,量子计算都只表示笑笑不说话。量子计算目前还在继续发展,当下,我们只能使用经典计算来写并发程序。

Coroutines

注:本节介绍的协程不具体指某一语言中的,示例亦为伪码,C++20 的协程将于第五节介绍。

弄清楚了并发概念,便可正式开始讲协程了,先来看看 wiki 上的定义:

Coroutines are computer program components that generalize subroutines for non-preemptive multitasking, by allowing execution to be suspended and resumed. Coroutines are well-suited for implementing familiar program components such as cooperative tasks, exceptions, event loops, iterators, infinite lists and pipes.

简言之,协程,用于实现协作式多任务,属于同一个进程的多个协程,在同一时刻只有一个处于运行状态。

协程属于并发形式的一种创建方式,它由两部分组成的:Functions 和 Cooperative。可以说,Functions 之间拥有了 Cooperative 能力,便称之为 Coroutines。

比如,现有一个普通的函数:

auto happy()
{
    return ": )";  // 在此处传值并退出函数
}

auto emoji = happy();  // 在此处调用函数

此函数返回一个字符串,可以直接进行输出。当调用函数时,只能等到函数执行结束才算结束调用。

现在对表情进行扩展。

void emoji()
{
    vector<string> emoji { ": )", ": (", "^-^", "@_@", "=^=" };
    for(auto p = emoji.begin(); p != emoji.end(); ++p)
    {
        cout << *p << " ";
    }
    cout << endl;
}

现在,我们想在不改变该函数的情况下进入如下输出:

1. : )
2. : (
3. ^-^
4. @_@
5. =^=

于是需要引出另一个函数:

void add_lines()
{
    for(int i = 1;; ++i)
    {
        cout << i << ": ";
        cout << endl;
    }
}

可是,即使如此,依旧无法得到想要的输出。此时,只有这两个函数互相协作才能完成任务。

Image

注意这两个函数的执行顺序,他们是交叉执行的,而非像普通函数那样调用后执行完直接返回。

这里有几个关键点。

第一,被调用函数(emoji)需要知道调用函数(add_lines)的返回地址,这样才能在输出后再跳回到调用方继续执行。

第二,需要记住先前局部变量的值,用于在下一次执行时恢复现场。

第三,被调用方可以主动让出控制流,以让调用方恢复执行。

第四,调用方可以主动恢复被调用方的执行,此时会从上次暂停的地方继续执行。

实际上,协程本质上就是对控制流的主动让出和恢复机制,这也是其与函数之间的区别。

协程是函数的增强版,函数是协程的弱化版。

函数只能启动(start)和终止(finish)执行,而协程在此基础上,增加了挂起(suspend)和恢复(resume)能力。

那么如何完成上述流程呢?

可以来增加挂起和恢复机制:

// !!!伪码表示,仅作说明之用

struct coro_frame {
    using iter_type = vector<string>::iterator;
    iter_type iter;
    int index;

    resume() {
        switch(index) {
        case 0:
            goto flag_r0;  // 从flag_r0恢复执行
        default:
            goto flag_default;    
        }
    }
};

coro_frame* emoji()
{
    void* handle = CORO_BEGIN(malloc);  // 分配状态所需的空间
    coro_frame* frame = (coro_frame*)handle;  // 转换为frame

    vector<string> emoji { ": )", ": (", "^-^", "@_@", "=^=" };
    for(frame->iter = emoji.begin(); frame->iter != emoji.end(); ++frame->iter)
    {
        frame.index = 0;  // 记录暂停点
        CORO_SUSPEND(handle);  // 暂停执行,挂起协程
        RETURN_OBJECT(frame);  // 返回控制句柄,以便从外部恢复
    flag_r0:
        print(frame->iter);
    }
    println();
    CORO_END(hdl, free);  // 协程结束,销毁申请的空间
}

void add_lines()
{
    auto frame = await emoji();
    for(int i = 1;; ++i)
    {
        println(i);
        frame.resume();  // 在此恢复被调用方
        println();
    }
}

可以看到,我们对原有代码添加了许多额外代码来满足挂起恢复的能力,实际上编译器在生成协程时也会有这样的操作,不过那是一个非常完善的机制了。

相信通过上述内容,相信大家已经对协程有了大致的印象,那么本节的任务也就完成了。

Stackful Coroutines versus Stackless Coroutines

函数在调用之前会将所需的参数压入栈中以供使用,局部的数据也都保留在栈中,这一系列所需的数据称为状态(state),需要在调用方和被调用方之间来保存这些状态,才能完成函数的调用和返回动作。

协程也需要保存一些状态,而且因为要支持挂起和恢复功能,所以要保存的状态也要比函数多。协程需要保存如下状态:

  • Promise 对象
  • 各个形参
  • 当前挂起点的位置,以便恢复时跳转
  • 局部变量和临时量

此时,便会有不同方式来保存这些状态。主流的两种,一种是有栈协程,另一种是无栈协程。

有栈协程,顾名思义,将数据保存在栈中;而无栈协程,会将主要数据采用堆来动态分配,只有少量状态存放在栈上,此时只能挂起处于停层的函数。

有栈协程的生命期和它们的栈一样长,无栈协程的生命期和它们的对象一样长;有栈协程的数据在被调用方分配,而无栈协程的数据在调用方分配。

关于此二者的区别,已经有文章详细描述,可以参考这篇文章:Coroutines Intruduction

其实现在了解与否并无关紧要,只需记往 C++20 的协程是无栈协程便可。

Distinguish between process, thread and coroutine

关于进程线程,过去已经写过一些文章了,此处便不再详细介绍,关键是需要弄清楚并发和并行的区别,这也在前文进行了详细介绍。

现在,只需下表的对比,就能清晰地了解这三者之间的关系与差别。

进程 线程 协程
切换者 操作系统 操作系统 用户(编程者/应用程序)
切换时机 根据操作系统自己的切换策略,用户不感知 根据操作系统自己的切换策略,用户不感知 用户自己(的程序)决定
切换内容 页全局目录
内存栈
硬件上下文
内核栈
硬件上下文
硬件上下文
切换内容的保存 保存于内核栈中 保存于内核栈中 保存于用户自己的变量(用户栈或堆)
切换过程 用户态-内核态-用户态 用户态-内核态-用户态 用户态(未陷入内核态)
切换效率

正是因为进程和线程的上下文切换效率低,所以才加入了协程来提高并发的能力。

C++20 Coroutines TS

First Look at C++ Coroutines

如前文所述,协程是一个分步执行,遇到条件会挂起,直到满足条件才会被唤醒以继续执行后面代码的并行方式。

C++ 准备了许久,终于在今年加入了协程,不过 C++20 的协程标准仍处于「原理层」,只包含了编译器需要实现的底层功能,是专门给库开发者使用的,并没有提供属于「应用层」的高级库给普通程序员使用。

这意味着要想使用协程,要么自己从原理层学起,自己封装所需的功能,要么等待 C++23 的标准协程库或第三方库。

我们自然选择前者,因此要学习的东西就比较多了,本篇就是先带大家入协程的门,后面再层层深入。

C++20 提供了三个新的关键字来支持协程:

  • co_await
  • co_yield
  • co_return

只要一个函数中包含了上面三个关键字中的任意一个,那么这个函数就是一个协程。co_await 可以挂起和恢复函数的执行,是主要需要学习的一个关键字;co_yield 可以在不结束协程的情况下从协程返回一些值。因此,可以用它来编写无终止条件的生成器函数;co_return 允许从协程返回一些值,不过这需要稍加定制。

比如一个生成器函数:

generator<int> generate_numbers(int begin, int inc = 1)
{
    for(int i = begin;; i += inc)
        co_yield i;
}

因为 generate_numbers() 中包含了 co_yield 关键字,所以它就是一个协程。你也许已经看到,该协程没有结束条件,因此它可以无限地产生值。

但就此一点代码可还无法完成工作,稍后再来完善它,在此之前,还需了解几个重要的组件。

Custom Coroutine Functor and Custom Promise

我们已经知道可通过 co_yield 来挂起协程,但是挂起之后又该如何恢复呢?

因此,需要有一种方式可以和协程进行沟通,当协程挂起的时候,通过这种方式来进行恢复。

这种方式便是 Custom Coroutine Functor(定制协程函数/仿函数),前面所写生成器的返回类型 generator 便是我们对于生成器所定义的 Coroutine Functor

注:前面使用了协程关键字的 generate_numbers 也叫协程函数,为避免和此处的 Coroutine Functor 翻译混淆,本文中所有协程函数都指前者,Coroutine Functor 均以英文出现。

现在,来定义它:

template <typename T> class generator
{
public:
    // 恢复协程
    bool resume();
};

在该 Coroutine Functor 中,声明了用于恢复协程的 resume 函数。

现在,进行编译程序,可以获取如下错误:

error.png

编译器告诉我们,promise_type 不是 generator 的成员,因此,在 Coroutine Functor 中需要遵循一些规范。

我们需要在成员中加入 promise_type,这个东西称为 Promise 对象,需要定义以下接口:

  • get_return_object
  • initial_suspend
  • final_suspend
  • return_void
  • un_handled_exception

当使用协程函数时:

auto nums = generate_numbers(1);

首先会挂起当前执行点,此时便会调用 initial_suspend。接着,通过 get_return_objectCoroutine Functor 返回给调用方,因此在稍后才能进行恢复。当遇到未处理的异常时,便会调用 un_handled_exception,可以在此进行捕获。结束时,会调用用 return_void,来到 final_suspend,协程结束,Coroutine Functor 析构。

由这些接口可知,协程通过 Promise 对象来提交结果或返回异常。

现在来实现这些接口:

template <typename T> class generator
{
public:
    struct promise_type {
        using coro_handle = std::experimental::coroutine_handle<promise_type>;
        auto get_return_object() {
            return coro_handle::from_promise(*this);
        }

        auto initial_suspend() { return std::experimental::suspend_always{}; }
        auto final_suspend() { return std::experimental::suspend_always{}; }

        auto return_void() {}

        void un_handled_exception() { std::terminate(); }
    };

    bool resume();
};

接口俱以实现。可以发现其中又新增了两个东西:

  • coroutine_handle
  • suspend_always

下一步之前,需要先了解这些概念。

Coroutine handle

Promise 对象从协程内部操纵,用于提交结果和异常,而 Coroutine handle(协程句柄)则从协程外部操纵,专门用于管理协程上下文,可以恢复或销毁协程。

与 Promise 对象不同,Coroutine handle 在标准文件已经定义:

template <>
struct coroutine_handle<void>;   // no promise access

template <typename _PromiseT>
struct coroutine_handle : coroutine_handle<>;  // general form

可以看到,有两个形式,一个 void 特化版和一个通用形式。

Coroutine handle 主要包含了以下几个重要成员:

  • static coroutine_handle from_address(void*)
  • void* address()
  • void operator()()
  • explicit operator bool()
  • void resume()
  • void destroy()
  • bool done()

Coroutine Functor 的恢复操作,其实就是通过 Coroutine handle 的 resume 函数进行恢复协程的,拥有了它,也就拥有了控制协程的能力。

既然 Coroutine handle 这么重要,那么如何为我们的 Coroutine Functor 配备上呢?

首先,编译器如何把 Coroutine handle 给我们?想要就得主动点,只需在 promise_type 中自己先定义一个 co_handle 类型:

struct promise_type {
    using coro_handle = std::experimental::coroutine_handle<promise_type>;
    auto get_return_object() {
        return coro_handle::from_promise(*this);
    }
...

由此,便给 promise_type 配上了 Coroutine handle,通过 from_promise 函数,可以从 promise 来创建 Coroutine handle。也可直接使用 return generator{};。之后,调用 get_return_object 函数便能得到匹配的 Coroutine handle。

前面说过,Promise 对象是从协程内部操纵的,因此 get_return_object 函数其实是给编译器调用的。

那么搞了半天,我们要怎么用呢?

当协程首次挂起时,编译器便会使用 operator new 分配空间,保存当前作用域内的各种信息,以在恢复时使用。随后便会调用 get_return_object(),并将此结果返回给调用方,也就是 Coroutine Functor。

因此,我们需要在那时进行保存。

template <typename T> class generator
{
public:
    struct promise_type {
        ...
    };
    using coro_handle = std::experimental::coroutine_handle<promise_type>;
    generator(coro_handle handle) : handle_(handle) { assert(handle_); }
    generator(const generator&) = delete;
    generator(generator&& other) : handle_(other.handle_) { other.handle_ = nullptr; };
    ~generator() { handle_.destroy(); }

    bool resume() {
        if (!handle_.done())
            handle_.resume();
        return !handle_.done();
    }

private:
    coro_handle handle_;
};

可以看到,编译器实际上调用了我们的构造函数,因此,必须提供 Coroutine handle 类型的构造函数。

之后,便可对协程进行操控。可以看到,当前已经可以使用 resume 函数来恢复协程了。

Awaitable object

Awaitable object 叫作可等待的对象,或者应该说是拥有等待属性的对象。

何时挂起,何时恢复,一定需要根据一个条件来决定。比如,当服务器接收数据时,在等待数据的时候便挂起,去执行别的逻辑。待数据到来,便恢复接收数据的操作。

这个对于条件的抽象便是 Awaitable object,需要满足三个接口:

  • bool await_ready()
  • void await_suspend(coroutine_handle<>)
  • auto await_resume()

每次调用 co_await 时,便会调用 await_ready() 来查看是否已满足条件,满足表示万事俱备,编译器会调用 await_resume() 恢复协程;若条件尚未准备完成,则会调用 await_suspend() 挂起协程,将控制权返还给调用者。

标准中提供了两个 trival Awaitable object

suspend_never
{
    bool await_ready() { return true; }
    void await_suspend(coroutine_handle<>) {}
    auto await_resume() {}
};

suspend_always
{
    bool await_ready() { return false; }
    void await_suspend(coroutine_handle<>) {}
    auto await_resume() {}
};

suspend_alwaysawait_ready() 总是返回 falsesuspend_neverawait_ready() 总是返回 true。通常情况这两个已经可以满足需求了,但也有很多情况需要自己编写特定版本,这些放到下篇专门来介绍。

Coroutine traits

回到生成器的协程函数:

generator<int> generate_numbers(int begin, int inc = 1)
{
    for(int i = begin;; i += inc)
        co_yield i;
}

编译器需要从返回类型 generator<int> 来确定 Promise 的类型,其所使用的萃取特性的工具便是 Coroutine traits(协程特性),标准中自带了一个 std::coroutine_traits,便是其实现。

所以我们的协程函数的 Promise 类型将被推导为:

std::coroutine_traits<generator<int>, int, int>::promise_type

co_yield, co_await, and co_return

到此为止,大家基本已经了解这三个关键字的概念。

接下来将继续完成 co_yield 所完成的生成器并介绍 co_returnco_await 是协程的关键角色,放到下篇专门来论。

因为要使用 co_yield,所以还需给 Promise 增加一个 yield_value 接口:

template <typename T> class generator
{
public:
    struct promise_type {
        ...
        T value_;
        auto yield_value(T value) {
            value_ = value;
            return std::experimental::suspend_always{};
        }
    };
};

yield_value 将数据保存到 Promise 中,取出操作须自己定义:

template <typename T> class generator
{
public:
    struct promise_type {
        ...
        T value_;
        // 保存生成的值
        auto yield_value(T value) {
            value_ = value;
            return std::experimental::suspend_always{};
        }
    };
    ...
    // 获取生成的值
    const T get_generated_value() {
        return handle_.promise().value_;
    }
private:
    coro_handle handle_;
};

yield_value 不同的是,get_generated_value 是自定义的,名称随意。

现在,便可以使用我们的协程了:

int main()
{
    auto nums = generate_numbers(1);
    for (;;)
    {
        nums.resume();
        std::cout << nums.get_generated_value() << " ";

        if (nums.get_generated_value() > 20) break;
    }
}

/*
 * Output:
   1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
*/

此处,使用生成器从 1 开始,依次以递增 1 的趋势生成数字,现在的结束条件完全在调用方这边。因此,所有流程,都可由调用方进行控制。

最后,简单地介绍下 co_return,它用于协程的返回,就像 return 一样,不过是专门针对协程的。

例如:

std::future<int> a() {
    co_return 42;
}

当协程没有结束条件时,就像上述定义的生成器,则需要定义 return_void()return_value(),若无定义,当协程结束时会遇到未定义的行为(一般没定义会直接编译不过)。

如果通过 co_return 返回 void,则会调用 return_void(); 通过 co_return 返回一些值时,则会调用 return_value()。这两个函数不能同时定义。

本节到此便已结束,相信大家对 C++ 的协程也有了初步的认识,后面便能再写一些稍微有难度的文章来继续分享。

本节完整的协程代码如下:

#include <iostream>
#include <cassert>

#include <experimental/coroutine>

template <typename T> class generator
{
public:
    struct promise_type {
        using coro_handle = std::experimental::coroutine_handle<promise_type>;
        auto get_return_object() {
            return coro_handle::from_promise(*this);
        }

        auto initial_suspend() { return std::experimental::suspend_always{}; }
        auto final_suspend() { return std::experimental::suspend_always{}; }

        auto return_void() {}

        void un_handled_exception() { std::terminate(); }

        T value_;
        auto yield_value(T value) {
            value_ = value;
            return std::experimental::suspend_always{};
        }
    };
    using coro_handle = std::experimental::coroutine_handle<promise_type>;
    generator(coro_handle handle) : handle_(handle) { assert(handle_); }
    generator(const generator&) = delete;
    generator(generator&& other) : handle_(other.handle_) { other.handle_ = nullptr; };
    ~generator() { handle_.destroy(); }

    bool resume() {
        if (!handle_.done())
            handle_.resume();
        return !handle_.done();
    }

    const T get_generated_value() {
        return handle_.promise().value_;
    }
private:
    coro_handle handle_;
};

generator<int> generate_numbers(int begin, int inc = 1)
{
    for (int i = begin;; i += inc)
        co_yield i;
}

int main()
{
    auto nums = generate_numbers(1);
    for (;;)
    {
        nums.resume();
        std::cout << nums.get_generated_value() << " ";

        if (nums.get_generated_value() > 20) break;
    }

    return 0;
}

Coroutines use cases

  • generators
  • 异步IO
  • lazy computations
  • 事件驱动程序
  • 协作式多任务
  • 无限列表
  • ……

Conclusion

本篇介绍了 C++20 协程的大多数概念,理解之后便算是协程入门了。

关于文章开头的那些问题,想必大家心中也已有了答案。

但这些介绍仍只是其冰山一角,C++ 的协程还有非常多的知识需要学习,待日后再慢慢道来。

References

Leave a Reply

Your email address will not be published. Required fields are marked *

You can use the Markdown in the comment form.