Coroutines in C++20

2021-03-19  本文已影响0人  Platanuses

首先,希望读者已经在其他语言或库中了解协程的概念。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,可返回以下三种类型:

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

标准库

头文件 <coroutine>
参考文档

上一篇下一篇

猜你喜欢

热点阅读