跳至正文

C++20协程写法细节

  • 随笔

前言

深入看了coroutine提案作者写的几篇文章,从协程的抽象到实现细节,真的有学到一些东西。但其实我是大概率不会自己基于C++20原生的接口,但是看到几个一定要记的东西,感觉还是可以写篇博客记录一下。

文章原文:
https://lewissbaker.github.io/2017/11/17/understanding-operator-co-await
https://lewissbaker.github.io/2018/09/05/understanding-the-promise-type

协程基础类

struct coroutine : std::coroutine_handle<promise>
{
    using promise_type = ::promise;
};

struct promise
{
    coroutine get_return_object() { return {coroutine::from_promise(*this)}; }
    std::suspend_always initial_suspend() noexcept { return {}; }
    std::suspend_always final_suspend() noexcept { return {}; }
    void return_void() {}
    void unhandled_exception() {}
};
coroutine bad2()
{
    S s{0};
    return s.f(); // returned coroutine can't be resumed without committing use after free
}

在上面这个例子里,首先协程本身的定义是有co_await, co_yield, co_return关键字的函数就是协程,是协程就需要满足这个条件:函数返回一个用户自定义的写成类型,我们叫做coroutine类型吧。

coroutine::promise_type是定义的Promise类,这个类是协程跟三个关键字交互的主要类,同时为了能够让已有的类(例如std::future<>)容易被融入协程,20提供了coroutine_traits这个模板。

对于一个协程:

task<float> foo(std::string x, bool flag);

编译器并不是用task<float>::promise_type用作Promise类,而是用的:typename coroutine_traits<task<float>, std::string, bool>::promise_type

这个的好处可以在cpprefernece的文档里看到好处:

// Utilize the infrastructure we have established.
std::future<int> compute(as_coroutine)
{
    int a = co_await std::async([] { return 6; });
    int b = co_await std::async([] { return 7; });
    co_return a * b;
}

我可以通过重写coroutine_traits的模板特化,实现将原有的普通类直接改造成异步类,然后不用重写原本的类,直接:

template<typename T, typename... Args>
    requires(!std::is_void_v<T> && !std::is_reference_v<T>)
struct std::coroutine_traits<std::future<T>, as_coroutine, Args...>
{
    struct promise_type : std::promise<T>
    {
        std::future<T> get_return_object() noexcept
        {
            return this->get_future();
        }

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

        void return_value(const T& value)
            noexcept(std::is_nothrow_copy_constructible_v<T>)
        {
            this->set_value(value);
        }

        void return_value(T&& value) noexcept(std::is_nothrow_move_constructible_v<T>)
        {
            this->set_value(std::move(value));
        }

        void unhandled_exception() noexcept
        {
            this->set_exception(std::current_exception());
        }
    };
};

这样就能修改promise类,而不用修改原本的普通类。

这大概也是设计者将return_typepromise_type区分开,并且通过coroutine_traits::promise_type拿到Promise类的目的。

coroutine_handle

coroutine_handle是C++底层的对象,它不是上面定义的std::future<int>,也不是它的promise_type,它是C++底层的协程栈相关的数据结构。

我们获取它只有两个途径,一个是在awaitable对象的await_suspend函数里能拿到被暂停协程的上下文,等需要恢复原协程时去控制。另外一个就是通过Promise对象的引用,通过静态函数coroutine::from_promise能获取到coroutine_handle对象。

coroutine_handle的主要功能就是控制写成本身的暂停、销毁和恢复。

这里有一个一定要注意的就是coroutine_handle对象不是raii对象,但是是一个类似于void*的类型,需要手动调用destroy!!!!

awaitable

co_await 后面跟的必须是一个awaitable对象,而awaitable对象是定义了这三个接口的类:

bool await_ready() { return false; }
void await_suspend(std::coroutine_handle<> h)
{
    std::jthread& out = *p_out;
    if (out.joinable())
        throw std::runtime_error("Output jthread parameter not empty");
    out = std::jthread([h] { h.resume(); });
    // Potential undefined behavior: accessing potentially destroyed *this
    // std::cout << "New thread ID: " << p_out->get_id() << '\n';
    std::cout << "New thread ID: " << out.get_id() << '\n'; // this is OK
}
void await_resume() {}

分别代表本次是否挂起(await_ready),挂起前的准备操作(await_suspend),并且将coroutine_handle传递进来,在稍后某些条件满足之后,如何通过coroutine_handle重新唤起这个被挂起的写成。而await_resume就是在协程重新恢复的时候需要执行的逻辑。

内存申请和释放

在这个提案处于争论状态的时候,攻击者就提到了这套协程会频繁在栈上申请空间,作者就提出了两个方案:

  1. 编译器判断不需要栈空间,就直接不特殊申请;
  2. 用户可以定制new和delete;

但是用户定制new和delete比较复杂,文章里是说当执行到delete之前,部分内存就已经被销毁了,所以在new和delete时都需要额外多分配一小段内存存储allocator对象本身。

比较复杂,记录一下。

总结

这是关于协程的第三篇文章了,OK不再继续看协程的内容了。
核心的思想理解了,核心的设计理解了,我也不会用原生的语义开发协程,毕竟asio等库有良好的封装。

经过不公正的测试,协程本身速度会比纯异步慢5%左右,但是拥有巨大的好处!
就是同步的方式写逻辑,同步的方式写逻辑最大的好处感觉有两个:

  1. 不用写特别多的lambda和函数;
  2. 部分变量可以放在协程上下文做管理,就不用关心生命周期了。

End

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

目录