上篇介绍了 C++20 协程的诸多内容,独余 co_await 未曾涉及,它是协程中非常重要的一个关键字,用以支持挂起(suspend)和恢复(resume)的逻辑。

本篇便专门来对其进行介绍。

Awaitable type and Awaiter type

较于普通函数,协程支持挂起和恢复。那么何时挂起,何时恢复,便是逻辑之所在。

由于许多问题在解决时都无法立刻得到答案,即结果存在延迟性,在程序中就表现为阻塞。阻塞会导致CPU大量空闲,效率大减,于是就要想办法实现非阻塞。

多线程便是解决阻塞的一个方式,遇到阻塞,便由操作系统进行切换调度,以此实现非阻塞。重叠 IO 亦是一个非阻塞方案,遇到阻塞,提供一个回调函数给操作系统,系统在阻塞完成后调用其继续执行。

这些方案,本质上都是在处理如何挂起和恢复的问题。换言之,就是在遇到阻塞时暂停当前工作,先去进行别的工作,等阻塞完成后再回来继续完成当前工作。

既然拥有共同的处理问题的逻辑,那么对其进行抽象,便能得到一个高层级的类型。这个高层级的类型便是 Awaitable type(即上篇的 Awaitable object)。

简单地说,Awaitable type 就是对阻塞问题进行总结、归纳、提炼要点,所得到的模型。

那么有何好处呢?

好处就是,我们只要依照抽象后所得模型中的一些规则,便能定义出所有类似问题的解决逻辑,所有类似问题都能依此模型进行解决。

乍听很复杂,其实并不难,只要依规则行事便可。

那么具体规则又是什么?

其实只是三个接口:

  • await_ready()
  • await_suspend()
  • await_resume()

它们分别代表着:是否阻塞、挂起、恢复。

将程序中阻塞完成的条件,写到 await_ready 函数中,便能依此决策何时挂起,何时恢复。

一个类型若直接实现了这三个接口,那么这个类就被称为 Awaiter

什么意思呢?若类型 A 本身并未实现这三个接口,而是通过类型 B 实现的,那么类型 A 就称作 Awaitable,类型 B 称作 Awaiter

若类型 A 直接实现了这三个接口,那么它既是 Awaitable,也是 Awaiter

Awaiter 是真正的逻辑所在,co_await 只是一个导火索,用来触发具体的语义,具体语义实际是由 Awaiter 进行控制的。

operator co_await

一元操作符 co_await 用于启动具体的行为逻辑,用法如下:

co_await exp

那么这个 exp 就是所谓的 Awaitable,它必须实现所需的接口。这么说来,co_await 就有两点作用:

  • 强制编译器生成一些样板代码。为的是完成相关的启动操作。
  • 创建 Awaiter 对象。为的是完成实际的逻辑。

创建 Awaiter 对象有两种方式。

第一种是重载 operator co_await,由此可通过返回值得到创建的 Awaiter。

第二种是在当前协程的 promise type 中定义 await_transform 函数,由此将相关类型转换为 Awaiter。

如果一个 exp 直接是 Awaiter,那么 Awaiter 就是 exp 本身。

获得了 Awaiter,便能根据 await_ready 来决策挂起和恢复的逻辑。具体细节,见于后文。

co_await 最终的返回结果,就是 await_resume() 的结果,Awaiter 会在 co_await 表达式结束前销毁。

现在,先来看一个简单的例子:

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

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

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

        int cur_value;
        void return_value(int value) {
            cur_value = value;
        }
    };

    using coro_handle = std::experimental::coroutine_handle<promise_type>;
    coroutine_type(coro_handle handle) : handle_(handle) { assert(handle_); }
    coroutine_type(const coroutine_type&) = delete;
    coroutine_type(coroutine_type&& other) : handle_(other.handle_) { other.handle_ = nullptr; }
    ~coroutine_type() { handle_.destroy(); }

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

    int get_result() {
        return handle_.promise().cur_value;
    }

private:
    coro_handle handle_;
};

coroutine_type coroutine()
{
    std::cout << "begin coroutine\n";
    co_await std::experimental::suspend_always{};
    std::cout << "resumed\n";
    co_return 42;
}

int main()
{
    auto coro = coroutine();
    coro.resume();
    std::cout << coro.get_result() << "\n";
}

/*
 * Output:
   begin suspendsion
   resumed
   42
*/

标准提供了两个 Trivial Awaitable object,一个是 suspend_always,另一个是 suspend_never。它们是可以直接使用的 Awaitable type,因为它们本身就满足所需的三个接口,所以既是 Awaitable,也是 Awaiter。

当执行时 co_await 时,解析步骤如下:

  1. 将表达式转换为 Awaitable,这里由于 suspend_always 本身便是 Awaiter,无需多做处理,最终获得到的 Awaiter 就是 suspend_always
  2. 获得到 Awaiter,便开始调用 await_ready(),由于 suspend_alwaysawait_ready() 总是返回 false,所以将调用 await_suspend() 挂起协程;
  3. 挂起协程,便会返回到调用方继续执行。此时,通过 reume() 便可恢复协程。
  4. 协程恢复,输出 "resumed",接着通过 co_return 返回 42co_return 会调用 promise type 中的 return_value(),在那对值进行保存;
  5. 协程返回,输出返回值,之后协程销毁。

逻辑框架与流程解析

由前文可知,co_await 的语义由 Awaitable object 提供的三个接口来进行控制,所有的操作全部都围绕着这三个接口进行执行。

那么我把这一系列的逻辑,称为协程的「逻辑框架」。绘制结构如图:

Framework of Coroutines

在这个逻辑框架中,没有任何多余的细节,它表示协程总体的逻辑流程。

当前的工作 current works,就是协程函数。其他的工作 other works,可以是普通函数,也可以是协程函数。

由 awaiter 所组成的「逻辑三角」,共同协调着当前工作与其它工作之间的切换。

而这一切的引子,便依赖于 co_await 关键字。一切的逻辑,便依赖于 ready(await_ready)。

ready 表示当前工作是否阻塞,准备完成意味着非阻塞,准备未完成意味着阻塞。非阻塞的情况,其实就相当于普通函数,此时会走 ③T 这条路。阻塞的情况,便需要挂起当前协程,切换到 other works。

那么具体的细节如何?看下面的代码描述。

{
    // 1. 将expr转换为Awaitable
    auto&& value = expr;
    auto&& awaitable = get_awaitable(promise, static_cast<decltype(value)>(value));

    // 2. 获取Awaiter
    auto&& awaiter = get_awaiter(static_cast<decltype(awaitable)>(awaitable));

    std::exception_ptr exception = nullptr;
    if(!awaiter.await_ready())  // 3. 判断是否准备完成
    {
        // 4. 挂起当前协程,此时所需的局部变量将被保存
        suspend_coroutine();

        // 5. 根据await_suspend的返回值,决定返回到何处

        // if await_suspend returns void
        try {
            awaiter.await_suspend(coroutine_handle);
            return_to_the_caller();
        } catch(...) {
            exception = std::current_exception();
            goto resume_point;
        }

        // if await_suspend returns bool
        try {
            await_suspend_result = awaiter.await_suspend(coroutine_handle);
        } catch(...) {
            exception = std::current_exception();
            goto resume_point;
        }
        if(!await_suspend_result)
            goto resume_point();
        return_to_the_caller();

        // if await_suspend returns another coroutine_handle
        decltype(await.await_suspend(std::declval<coro_handle_t>())) another_coro_handle;
        try {
            another_coro_handle = awaiter.await_suspend(coroutine_handle);
        } catch(...) {
            exception = std::current_exception();
            goto resume_point;
        }
    }

resume_point:
    if(exception)
        std::rethrow_exception(exception);  // await_resume将不会被调用
    a.await_resume();  // 6. the end, 恢复当前协程
}

首先,当遇到有 co_await 修饰的表达式时,编译器便知该函数是一个协程。于是,就尝试获取 Awaitable,通过 Awaitable 再得到 Awaiter。

其次,通过 Awaiter 进行实质的逻辑操作。先调用 await_ready() 检测是否准备完成,准备完成则直接调用 await_resume() 恢复执行。若不满足条件,那么就需要挂起协程,转去执行其它工作。

此时就需要调用 await_suspend() 并返回控制给调用者,这决定了程序接下来的控制流去向。

await_suspend() 有三种返回值:

  • 如果返回值为 void,将会直接跳转到当前协程的调用方;
  • 如果返回值为 bool,那么为 true 时,会跳转到当前协程的调用方;为 false 时,会恢复当前协程;
  • 如果返回值为 coroutine handle,那么就会跳转到其它的协程,亦即其它协程将被恢复。

协程跳转后,便会到达 other works,恢复操作在这里控制,只要通过 resume() 便能恢复协程。由此,就构成了一个循环。当协程函数执行完毕或通过 co_return 返回,将打破循环。

Timer awaiter

现在,来看最后一个例子,也比较简单。

这里我们将自定义一个简单的定时器,即时间一到,才会执行下面的逻辑;时间没到,切出去执行别的工作。

#include <coroutine>
#include <iostream>

struct my_timer {
    int duration;
    std::coroutine_handle<> handle;
    my_timer(int d) : duration(d), handle(nullptr) {}
};

class timer_awaiter
{
public:
    my_timer& timer;
    timer_awaiter(my_timer& t) : timer(t) {}

    bool await_ready() noexcept {
        std::cout << "timerawaiter::await_ready()\n";
        return timer.duration <= 0;
    }

    void await_suspend(std::coroutine_handle<> handle) noexcept {
        timer.handle = handle;

        std::cout << "timer::await_suspend(), duration==" << (--timer.duration) << std::endl;
    }

    void await_resume() noexcept {
        std::cout << "timerawaiter::await_resume()\n";
        timer.handle = nullptr;
    }
};

struct coro_task
{
    struct promise_type {
        auto get_return_object() {
            return coro_task{};
        }

        auto initial_suspend() { return std::suspend_never{}; }
        auto final_suspend() noexcept { return std::suspend_never{}; }

        void unhandled_exception() { std::terminate(); }
        void return_void() {}
    };
};

auto operator co_await(my_timer& t) noexcept {
    return timer_awaiter{ t };
}

coro_task coro_test_func(my_timer& timer)
{
    std::cout << "begin coro_test_func()\n";

    co_await timer;
    co_await timer;  // 只会在前两次切出协程
    co_await timer;
    co_await timer;

    std::cout << "end of coro_test_func()\n";
}

int main()
{
    my_timer timer(2);
    coro_test_func(timer);

    for (; timer.handle && !timer.handle.done();)
    {
        std::cout << "in main for loop\n";

        // 恢复协程
        timer.handle.resume();
    }

    return 0;
}

这里,定义了一个 Awaiter timer_awaiter,此时的 Awaitable 是 my_timer,通过重载 operator co_await,其返回值便是由 Awaitable 得到的 Awaiter。

当定时器时间为 0 时,计时结束,之后 await_ready 将不满足条件,所以之后的调用都不会切出,而是直接返回到当前协程。

恢复协程的权力在调用方这里,所以需要提供 Coroutine handle,这是通过 await_suspend 传递进来的。

我们的 await_suspend 返回值为 void,所以便会直接返回到调用方。

其他东西前面都已见过,便不细述,输出结果为:

begin coro_test_func()
timerawaiter::await_ready()
timer::await_suspend(), duration==1
in main for loop
timerawaiter::await_resume()
timerawaiter::await_ready()
timer::await_suspend(), duration==0
in main for loop
timerawaiter::await_resume()
timerawaiter::await_ready()
timerawaiter::await_resume()
timerawaiter::await_ready()
timerawaiter::await_resume()
end of coro_test_func()

Conclusion

本篇介绍了 C++ Coroutine 中的 co_await 关键字,内容不多,但可说是协程中非常关键的东西。

文中所涉例子也比较简单,它们全都是在帮助你理解协程的「逻辑框架」,这也是本文中心之所在。

至此,协程的基础知识全部介绍完毕,大家现在也可以用协程来编写属于自己的程序了。顺手之后,便会觉得协程其实并不难。

若再要进行学习,就需要看一些协程库的设计了,有时间了再来写吧。

最后,本篇内容皆属个人理解,可能有错误之处,如觉哪里不妥,欢迎直接指正。

References

Leave a Reply

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

You can use the Markdown in the comment form.