Post

C++ 20 Coroutine

C++20四大件之——协程(coroutine)

C++ 20 Coroutine

简述

协程是用户态的线程,内核不知道协程的存在,其上下文的切换完全由应用程序自身决定,而不需要内核的参与,因此其性能会比线程更好

线程需要依赖于操作系统,比如linux中依赖于线程库pthread;但是协程不需要操作系统的支持,其完全由应用程序自己来实现

coroutine

c++20新增了coroutine ,coroutine没有显式的声明,任何包含co_await/co_yield/co_return的函数都能够成为协程,一个典型的coroutine包含以下几个要素

  • co_await/co_yield/co_return
  • returnType,即函数的返回类型,该返回类型中需要包含名为promise_type(名称必须保持一致)的嵌套类型
  • promise_type一般用于在协程内部维护一些数据,处理异常

coroutine_handle<>

  • 协程的句柄,给协程的调用者访问协程
  • 假设定义coroutine_handle<> h,则可以通过h()来激活(invoke)协程,h()等价于h.resume()
  • 通过h.destroy()来释放协程所占用的内存空间

co_await和awaiter(awaitable object)

  • 对于co_await而言,其等待的对象被称为awaiter或者(awaitable object),该对象决定了在执行co_await时协程是否要被挂起,以及后续的一些步骤
  • awaiter必须包含3个函数,await_readyawait_suspendawait_resume,其调用顺序如下
    • 首先调用await_ready来判断原始协程是否需要被挂起,如果await_ready返回false,则继续执行await_suspend,本质上是通过awaiter调用await_suspend方法,即awaiter.await_suspend(h)h为当前协程的句柄
    • 如果await_suspend的返回类型为bool类型,则需要进一步判断await_suspend的返回值是否为true,如果同时满足await_ready返回值为false以及await_suspend返回值为true,则协程会被挂起,直到被invoke
    • 在恢复控制权到协程前,会执行await_resume
  • <coroutine>中给出了两个默认的awaiter实现,即std::suspend_alwaysstd::suspend_never,顾名思义,前者总是会挂起当前协程,而后者不会挂起当前协程

promise object

  • 上面说到协程的返回类型必须要有一个名为promise_type的嵌套类型,promise_type中包含名为get_return_object的函数,其返回类型为外部类型ReturnType,即返回ReturnType类型的对象

  • get_returun_object在首次调用协程时被调用,将返回对象给予协程的调用者, 使得调用者得以访问访问协程

  • 一般来说返回类型会定义类型转化运算符,用于将当前返回类型转化为std::coroutine_handle<promise_type>类型,从而让调用者直接获取协程的句柄

  • 如果协程句柄为std::coroutine_handle<promise_type>,可以通过h.promise()获取promise object,如果是类型实参为空,则没有该promise()方法

  • 如果要在promise_type内部获取协程的句柄,可以通过静态函数std::coroutine_handle<promise_type>::from_promise(*this)来获取

  • 其他方法

    • initial_suspend():指定协程在启动时是否被挂起,返回std::suspend_always则挂起,返回std::suspend_never则不挂起

      1
      2
      
      std::suspend_always initial_suspend();
      std::suspend_never initial_suspend();
      
    • final_suspend():指定协程结束前是否挂起,挂起则可以通过句柄获取返回结果,否则直接将协程销毁

      1
      2
      
      std::suspend_always final_suspend() noexcept;
      std::suspend_never final_suspend() noexcept;
      
    • unhandled_exception():协程内部发生未捕获异常时,调用此函数处理异常,通常将异常存储到 std::exception_ptr 中,供调用方访问。

      1
      
      void unhandled_exception();
      
    • return_void():当协程中有co_return;时需要有该方法,调用co_return时会调用此方法

      1
      
      void return_void();
      
    • return_value(valueType value):当协程中有co_return value;时需要有该方法

      1
      
      void return_value(valueType value);
      
    • yield_value(valueType value):当协程中有co_yield value;时需要有该方法

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
#include <coroutine>
#include <iostream>

template <typename PromiseType>
//  用于返回promise地址的awaiter
struct GetPromise
{
    PromiseType *_p;
    bool await_ready() { return false; }
    //  本质上就是要捕获这个句柄h,然后额外定义了一个awaiter来获取协程句柄,然后通过协程句柄获取promise object
    bool await_suspend(std::coroutine_handle<PromiseType> h) //  通过协程句柄获取promise object
    {
        _p = &h.promise();
        return false; //  只有当await_ready返回false且await_suspend返回false时,协程才会被挂起
    }
    PromiseType *await_resume() { return _p; } //  控制权恢复到协程时调用,返回指向协程句柄的promise object的指针
};

struct ReturnObject3
{
    struct promise_type
    {
        unsigned* value_;

        ReturnObject3 get_return_object()
        {
            return ReturnObject3{
                .h_ = std::coroutine_handle<promise_type>::from_promise(*this)};  //  由promise_type获取其所归属的协程句柄
        }
        std::suspend_never initial_suspend() { return {}; }
        std::suspend_never final_suspend() noexcept { return {}; }
        void unhandled_exception() {}
    };

    std::coroutine_handle<promise_type> h_;
    operator std::coroutine_handle<promise_type>() const { return h_; }  //  类型转换运算符重载,将ReturnObject3转化为std::coroutine_handle<promise_type>
};

ReturnObject3 counter3()
{
    auto pp = co_await GetPromise<ReturnObject3::promise_type>{};  //  这里通过非阻滞的方式获取当前协程的promise object,具体执行过程如下
    /**
     * 首先调用GetPromise::await_ready()检查是否需要挂起当前协程
     * 然后调用GetPromise::await_suspend(h),h为当前协程counter3的协程句柄
     * 由于该函数返回值为false,因此协程counter3不会被挂起,则将控制权恢复到协程counter3
     * 恢复控制权前执行GetPromise::await_resume(),返回指向协程的promise obeject的指针
     * 最后将控制权转交给协程counter3
     */

    for (unsigned i = 0;; ++i)
    {
        pp->value_ = &i;
        co_await std::suspend_always{};  //  <coroutine>中默认的挂起awaiter,其默认会挂起当前协程,等待协程下一次被激活(invoke)
    }
}

int main()
{
    std::coroutine_handle<ReturnObject3::promise_type> h = counter3();  //  调用了类型转换运算符,由ReturnObject3类型转化到std::coroutine_handle<ReturnObject3::promise_type>
    ReturnObject3::promise_type &promise = h.promise();
    for (int i = 0; i < 3; ++i)
    {
        std::cout << "counter3: " << *promise.value_ << std::endl;
        h();  //  通过协程句柄h,invoke协程h
    }
    h.destroy();  //  释放协程的内存空间(能否用RAII封装或者智能指针),不释放会内存泄漏(memory leak)
}

co_yield

  • 上述实现过于臃肿,为了在协程中获取promise object,我们额外定义了awaiterGetPromise用于获取当前协程的句柄,然后通过此句柄调用promise()方法来获取promise object

  • 实际上,大可不必这么麻烦,通过co_yield就可以触发promise_type中的yield_value方法,来对promise object中的数值进行操作,而无需在协程中获取promise object

  • co_yield value等价于co_await promise_object.yield(value)

  • 通过co_yield修改上述代码如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    
    #include <coroutine>
    #include <iostream>
    //  整体需求:实现协程,此协程无限生成递增的序列
    //  协程不负责输出,只负责生成序列
    //  输出的工作仅在主函数中执行
      
    struct ReturnObject 
    {
        struct promise_type 
        {
            ReturnObject get_return_object()
            {
                return {
                    .coroutine_handle = std::coroutine_handle<promise_type>::from_promise(*this)
                };
            }
            std::suspend_never initial_suspend() { return {}; }
            std::suspend_never final_suspend() noexcept { return {}; }
            void unhandled_exception() {}
            std::suspend_always yield_value(unsigned int yield_value) 
            {
                value = yield_value;
                return {};
            }
            unsigned int value;
        };
        std::coroutine_handle<promise_type> coroutine_handle;  //  重载类型转换运算符operator type() {},将当前类型转化为协程句柄类型
        operator std::coroutine_handle<promise_type>() 
        {
            return coroutine_handle;
        }
    };
      
    ReturnObject counter() 
    {
        //  需要在协程获取promise对象
        for (unsigned int i = 0;;i++)
        {
            co_yield i;
        }
    }
      
    int main()
    {
        std::coroutine_handle<ReturnObject::promise_type> h = counter();
      
        for (unsigned int i = 0; i < 3; i++)
        {
            std::cout << "counter value: " << h.promise().value << std::endl;
            h();
        }
        h.destroy();
    }
    

co_return

  • co_return用于结束协程时执行

  • co_return;等价于调用promise_object.return_void();

  • co_return value;等价于调用promise_object.return_value(value);

  • 对于上述代码,如果不想在主函数(即协程的调用者)一方决定何时结束协程,而由协程自己决定,则可以使用co_return来实现,而协程的调用者只需通过h.done()来判断协程是否结束

  • 显示调用co_return;时,如果promise_type中没有实现return_void会给出提示,即编译不会通过

  • 在协程的末尾会默认调用co_return,但是如果在promise_type中没有定义return_void,则会产生ub(undefined behavior)

  • 因此,最好在promise_type中给出return_void的实现

  • 修改后的代码如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    
    #include <coroutine>
    #include <iostream>
    //  整体需求:实现协程,此协程无限生成递增的序列
    //  协程不负责在输出,只负责生成序列
    //  输出的工作仅在主函数中执行
      
    struct ReturnObject
    {
        struct promise_type
        {
            ReturnObject get_return_object()
            {
                return {
                    .coroutine_handle = std::coroutine_handle<promise_type>::from_promise(*this)};
            }
            std::suspend_never initial_suspend() { return {}; }
            std::suspend_never final_suspend() noexcept { return {}; }
            void unhandled_exception() {}
            std::suspend_always yield_value(unsigned int yield_value)
            {
                value = yield_value;
                return {};
            }
            void return_void() {}
            unsigned int value;
        };
        std::coroutine_handle<promise_type> coroutine_handle; //  重载类型转换运算符operator type() {},将当前类型转化为协程句柄类型
        operator std::coroutine_handle<promise_type>()
        {
            return coroutine_handle;
        }
    };
      
    ReturnObject counter()
    {
        //  需要在协程获取promise对象
        for (unsigned int i = 0; i < 3; i++)
        {
            co_yield i;
        }
        co_return;  //  这句话可以省略
    }
      
    int main()
    {
        std::coroutine_handle<ReturnObject::promise_type> h = counter();
        while (!h.done())
        {
            std::cout << "counter value: " << h.promise().value << std::endl;
            h();
        }
        h.destroy();
    }
    
  • 对于co_return后协程是否被销毁,是由promise_type的final_suspend决定的,如果final_suspend()返回std::suspend_always则协程不会被销毁,返回std::suspend_never则协程会销毁,一个协程内部的执行流程如下述伪代码所述

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    
    {
            promise-type promise promise-constructor-arguments ;//调用promise_type的构造函数
            try {
                co_await promise.initial_suspend() ;
                function-body  //  协程的函数体
            } catch ( ... ) {
                if (!initial-await-resume-called)
                    throw ;
                promise.unhandled_exception() ;
            }
        final-suspend :
            co_await promise.final_suspend() ;
    }
    // "The coroutine state is destroyed when control flows
    //  off the end of the coroutine"
    

参考资料

cppreference coroutine

My tutorial and take on C++20 coroutines

This post is licensed under CC BY 4.0 by the author.

Trending Tags