Coroutines in C++20

首先,希望读者已经在其他语言或库中了解协程的概念。C++20 终于带来了官方的协程,这是一种无栈的协程实现。

promise / yield / return

首先来看一个例子。这段代码建议从下往上看。

#include <iostream>
#include <coroutine>

template<typename T> struct Generator {
    struct promise_type;
    using Handle = std::coroutine_handle<promise_type>;

    struct promise_type {
        T val;

        Generator 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();
        }

        auto yield_value(T &&val) {
            this->val = std::move(val);
            return std::suspend_always();
        }
    };

    Generator(Generator const &) = delete;

    Generator(Generator &&g) : h(g.h) {
        g.h = nullptr;
    }

    ~Generator() {
        if (h) h.destroy();
    }

    bool go() {
        return h ? (h.resume(), !h.done()) : false;
    }

    T& val() {
        return h.promise().val;
    }

private:
    Handle h;

    Generator(Handle h): h(h) {}
};

Generator<int> f() {
    co_yield 1;
    co_yield 2;
}

int main() {
    auto g = f();
    while (g.go()) {
        std::cout << g.val() << std::endl;
    }
}

当一个函数的函数体中出现了关键字 co_yieldco_returnco_await,该函数则成为一个协程函数。上面的例子中,函数 f 便是一个协程函数。协程函数中不能有 return 语句,因为协程函数中的代码是协程执行时运行的代码,而在 main 函数中的第一行 auto g = f(),我们得到的返回值是一个 Generator 类型的生成器对象,但这并不是协程函数中的某个返回语句所返回的。总体来讲,上面的代码中,我们获得一个生成器对象 Generator g 来控制协程函数 f 的执行,可将 main 函数视为主协程,而 f 则成为其子协程。每次调用 g.go(),会从主协程切换到子协程,协程函数 f 开始执行,直到 co_yield 语句,子协程暂停运行,切换回主协程,此时我们在主协程中调用 g.val() 来获取传给 co_yield 语句的值。再次调用 g.go(),子协程会在上次暂停的地方恢复运行。如此,上面的代码会先后打印出 12

可以看到,不同于其他一些语言中由官方提供生成器实现,在 C++20 的协程中,所谓的生成器是由用户自行封装,而官方提供的是更为底层的接口,这让用户可以封装出更为灵活而丰富的实现,不局限于典型的生成器。用户自行定义一个控制协程函数执行的结构,在协程函数中声明返回此结构,此结构中必须包含一个名为 promise_type 的成员类型,是的只能叫这个名字。

promise_type 中必须定义 get_return_object 方法。形如 auto g = f() 的调用中,编译器会插入代码来构造一个协程函数的返回类型对应的 promise_type 对象,如果协程函数带有参数,则参数也会成为 promise_type 构造函数的参数。再调用其 get_return_object 方法生成一个协程函数的返回对象。这里,我们在 get_return_object 方法中让 promise_type 对象纳入到一个 std::coroutine_handle 对象中,再将 std::coroutine_handle 对象纳入到 Generator 对象中。Generator 通过 std::coroutine_handle 来控制协程的执行,其中,go 方法中调用 std::coroutine_handleresume 方法让协程开始或再次运行。

promise_type 中必须定义 initial_suspend 方法。调用 get_return_object 方法后,编译器插入的代码会继续调用 initial_suspend 方法,以控制协程一开始的行为。这里,我们返回一个 std::suspend_always 对象,让协程先暂停,而在第一次调用 std::coroutine_handleresume 方法后协程才真正第一次执行。如果换成 std::suspend_never,则协程会立即开始执行,直到第一次 co_yield 之后才会暂停,从而形如 auto g = f() 的调用返回。类似的,final_suspend 方法在协程函数执行结束之后自动调用,特别的,如果这里返回 std::suspend_never 则协程继续执行从而 segfault。

如果协程函数中出现了 co_yield 语句,则 promise_type 中必须定义 yield_value 方法,来处理 co_yield 语句传过来的值,同时控制协程的执行。这里,我们将 co_yield 的值移动赋值给 promise_type 中的字段,让 Generator 来访问,并且让协程暂停。

类似的,co_return 语句也可以传一个值,此时 promise_type 中必须定义 return_value 方法,来处理 co_return 语句传过来的值,但不能在这里控制协程的执行,而是接下来由 final_suspend 方法控制。与 co_yield 语句不同,co_return 可以不传值,此时 promise_type 中必须定义 return_void 方法。如:

void return_value(T &&val) {
    this->val = std::move(val);
}

void return_void() {}

协程创建时,以下对象会在堆上分配并构造:promise_type 对象、协程函数的非引用类型的形参、生命周期跨越暂停点的局部变量、一个记录协程执行情况的对象。这些对象在协程销毁时析构并释放内存。

await

co_await 是比 co_yieldco_return 更底层更泛化的操作。

在协程函数中,函数开头相当于插入 co_await promise.initial_suspend()co_yield 1; 相当于 co_await promise.yield_value(1);co_return 1; 相当于 promise.return_value(1); co_await promise.final_suspend(); return;,函数结尾相当于插入 co_await promise.final_suspend(); return;

co_await exp 操作首先确定一个可等待对象:对于协程函数开头、结尾自动插入的,以及由 co_yieldco_return 而来的 co_await 操作,可等待对象为传入的表达式 exp 自身;否则,如果当前协程的 promise_type 包含 await_transform 方法,则可等待对象为 promise.await_transform(exp);否则,可等待对象为传入的表达式 exp 自身。然后确定一个等待器对象:如果可等待对象定义了 co_await 操作符重载,则等待器为操作符重载的返回对象,否则等待器为可等待对象自身。

例如,std::suspend_always 一种可能的实现如下:

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

等待器类型必须实现以上三个方法。co_await 操作会立即调用 await_ready 方法:如果返回 true 则当前协程不暂停;否则协程暂停,返回到父协程,调用 await_suspend 方法。

await_suspend 方法传入调用 co_await 的协程的 coroutine_handle,可返回以下三种类型:

  • void:无其他操作,父协程继续运行。
  • booltrue 则父协程继续运行,false 则从父协程恢复到调用 co_await 的协程。
  • std::coroutine_handle:从父协程恢复到 handle 对应的协程。

如果确定协程不暂停,或者经过暂停之后被恢复,则调用 await_resume 方法。await_resume 方法的返回值也成为 co_await 操作符的返回值。

标准库

头文件 <coroutine>
参考文档

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,491评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,856评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,745评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,196评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,073评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,112评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,531评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,215评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,485评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,578评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,356评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,215评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,583评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,898评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,497评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,697评论 2 335

推荐阅读更多精彩内容