美文网首页
Coroutines in C++20

Coroutines in C++20

作者: Platanuses | 来源:发表于2021-03-19 18:18 被阅读0次

    首先,希望读者已经在其他语言或库中了解协程的概念。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>
    参考文档

    相关文章

      网友评论

          本文标题:Coroutines in C++20

          本文链接:https://www.haomeiwen.com/subject/leuvcltx.html