C++20协程

C++20协程

简介

​ C++20协程只是提供协程机制,而不是提供协程库。C++20的协程是无栈协程,无栈协程是一个可以挂起/恢复的特殊函数,是函数调用的泛化,且只能被线程调用,本身并不抢占内核调度。

​ C++20 提供了三个新关键字(co_await、co_yield 和 co_return),如果一个函数中存在这三个关键字之一,那么它就是一个协程。

​ 协程相关的三个关键字:co_await、co_yield与co_return

  • co_yield some_value: 保存当前协程的执行状态并挂起,返回some_value给调用者
  • co_await some_awaitable: 如果some_awaitable没有ready,就保存当前协程的执行状态并挂起
  • co_return some_value: 彻底结束当前协程,返回some_value给协程调用者

协程相关的对象

协程帧(coroutine frame)

当 caller 调用一个协程的时候会先创建一个协程帧,协程帧会构建 promise 对象,再通过 promise 对象产生 return object。

协程帧中主要有这些内容:

  • 协程参数

  • 局部变量

  • promise 对象

这些内容在协程恢复运行的时候需要用到,caller 通过协程帧的句柄 std::coroutine_handle 来访问协程帧。

promise_type

promise_type 是 promise 对象的类型。promise_type 用于定义一类协程的行为,包括协程创建方式、协程初始化完成和结束时的行为、发生异常时的行为、如何生成 awaiter 的行为以及 co_return 的行为等等。promise 对象可以用于记录/存储一个协程实例的状态。每个协程桢与每个 promise 对象以及每个协程实例是一一对应的。

promise_type的接口

promise_type接口 功能
initial_suspend() 控制协程初始化完成后是否挂起
final_suspend() 控制协程执行完后是否挂起
get_return_object() 返回给 caller 一个对象
unhandled_exception() 处理异常
return_void() 调用co_return;时或者协程执行完后被调用
return_value(T) 保存协程返回值。调用co_return xxx;的时候被调用,保存协程返回值
yield_value() 调用co_yield xxx;的时候,会调用,保存协程返回值
await_transform() 用于定制协程body中co_await xxx;语句的行为。定义该方法后,编译器会将出现在协程主体中的每个co_await xxx;转换为co_await promise.await_transform(xxx)

promise_type里的接口需要我们实现,promise_type里的接口是给编译器调用的

coroutine return object

它是promise.get_return_object()方法创建的,一种常见的实现手法会将 coroutine_handle 存储到 coroutine object 内,使得该 return object 获得访问协程的能力。

std::coroutine_handle

协程帧的句柄,主要用于访问底层的协程帧、恢复协程和释放协程帧。
程序员可通过调用 std::coroutine_handle::resume() 唤醒协程。

std::coroutine_handle接口

coroutine_handle接口 作用
from_promise() 从promise对象创建一个coroutine_handle
done() 检查协程是否运行完毕
operator bool 检查当前句柄是否是一个coroutie
operator() 恢复协程的执行
resume 恢复协程的执行(同上)
destroy 销毁协程
promise 获取协程的promise对象
address 返回coroutine_handle的指针
from_address 从指针导入一个coroutine_handle

coroutine_handle的接口不需要我们实现,可以直接调用

co_await、awaiter、awaitable

  • co_await:一元操作符;

  • awaitable:支持 co_await 操作符的类型;

  • awaiter:定义了 await_ready、await_suspend 和 await_resume 方法的类型。

co_await expr(expr是表达式) 通常用于表示等待一个任务(可能是 lazy 的,也可能不是)完成。co_await expr 时,expr 的类型需要是一个 awaitable,而该 co_await表达式的具体语义取决于根据该 awaitable 生成的 awaiter。

协程对象如何协作

以一个简单的代码展示这些协程对象如何协作:

Return_t foo () { 
    auto res = co_await awaiter; 
    co_return res ; 
}

Return_t:promise return object。

awaiter: 等待一个task完成。

图中浅蓝色部分的方法就是 Return_t 关联的 promise 对象的函数,浅红色部分就是 co_await 等待的 awaiter。

这个流程的驱动是由编译器根据协程函数生成的代码驱动的,分成三部分:

  • 协程创建;
  • co_await awaiter 等待 task 完成;
  • 获取协程返回值和释放协程帧。

协程的创建

Return_t foo () { 
    auto res = co_await awaiter; 
    co_return res ; 
}

foo()协程会生成下面这样的模板代码(伪代码),协程的创建都会产生类似的代码:

{
	co_await promise.initial_suspend();
  	try
  	{
    	coroutine body;
  	}
  	catch (...)
  	{
    	promise.unhandled_exception();
  	}
FinalSuspend:
  	co_await promise.final_suspend();
}

首先需要创建协程,创建协程之后是否挂起则由调用者设置 initial_suspend 的返回类型来确定。

创建协程的流程大概如下:

  • 创建一个协程帧(coroutine frame)
  • 在协程帧里构建 promise 对象
  • 把协程的参数拷贝到协程帧里
  • 调用 promise.get_return_object() 返回给 caller 一个对象,即代码中的 Return_t 对象

在这个模板框架里有一些可定制点:如 initial_suspend、final_suspend、unhandled_exception 和 return_value。

我们可以通过 promise 的 initial_suspend 和 final_suspend 返回类型来控制协程是否挂起,在 unhandled_exception 里处理异常,在 return_value 里保存协程返回值。

可以根据需要定制 initial_suspend 和 final_suspend 的返回对象来决定是否需要挂起协程。如果挂起协程,代码的控制权就会返回到caller,否则继续执行协程函数体(function body)。

PS:如果禁用异常,那么生成的代码里就不会有 try-catch。此时协程的运行效率几乎等同非协程版的普通函数。

co_await 机制

co_await 操作符是 C++20 新增的一个关键字,co_await expr 一般表示等待一个惰性求值的任务,这个任务可能在某个线程执行,也可能在 OS 内核执行,什么时候执行结束不知道,为了性能,我们又不希望阻塞等待这个任务完成,所以就借助 co_await 把协程挂起并返回到 caller,caller 可以继续做事情,当任务完成之后协程恢复并拿到 co_await 返回的结果。

所以 co_await 一般有这几个作用:

  • 挂起协程;
  • 返回到 caller;
  • 等待某个任务(可能是 lazy 的,也可能是非 lazy 的)完成之后返回任务的结果。

编译器会根据 co_await expr 生成这样的代码:

{
    auto&& value = <expr>;
    auto&& awaitable = get_awaitable(promise, static_cast<decltype(value)>(value));
    auto&& awaiter = get_awaiter(static_cast<decltype(awaitable)>(awaitable));
    if (!awaiter.await_ready()) //是否需要挂起协程
  	{
    	using handle_t = std::experimental::coroutine_handle<P>;
 
    	using await_suspend_result_t = decltype(awaiter.await_suspend(handle_t::from_promise(p)));
 
    	<suspend-coroutine> //挂起协程
 
    	if constexpr (std::is_void_v<await_suspend_result_t>)
    	{
      		awaiter.await_suspend(handle_t::from_promise(p)); //异步(也可能同步)执行task
      		<return-to-caller-or-resumer> //返回给caller
    	}
    	else
    	{
      		static_assert(
         		std::is_same_v<await_suspend_result_t, bool>,
         		"await_suspend() must return 'void' or 'bool'.");
 
      		if (awaiter.await_suspend(handle_t::from_promise(p)))
      		{
        		<return-to-caller-or-resumer>
      		}
    	}
 
    	<resume-point> //task执行完成,恢复协程,这里是协程恢复执行的地方
  	}
 
	return awaiter.await_resume(); //返回task结果
}

这个代码执行流程就是“协程运行流程图”中粉红色部分,从这个生成的代码可以看到,通过定制 awaiter.await_ready() 的返回值就可以控制是否挂起协程还是继续执行,返回 false 就会挂起协程,并执行 awaiter.await_suspend,通过 awaiter.await_suspend 的返回值来决定是返回 caller 还是继续执行。

正是 co_await 的这种机制是变“异步回调”为“同步”的关键。

C++20 协程中最重要的两个对象就是 promise 对象(恢复协程和获取某个任务的执行结果)和 awaiter(挂起协程,等待task执行完成),其它的都是“工具人”,要实现想要的的协程,关键是要设计如何让这两个对象协作好。

例子

#include <iostream>
#include <coroutine>
#include <thread>

namespace Coroutine {
    struct task {
        struct promise_type {
            promise_type() {
                std::cout << "1.task-promise_type():create promise object\n";
            }

            task get_return_object() {
                std::cout << "2.task-get_return_object():create coroutine return object, and the coroutine is created now\n";
                return {std::coroutine_handle<task::promise_type>::from_promise(*this)};
            }

            // initial_suspend()决定协程初始化后,是继续直接继续执行协程,还是挂起协程返回caller
            // 返回std::suspend_never,表示不挂起协程,会继续执行协程函数体(coroutine body)
            // 返回std::suspend_always,表示挂起协程,不会去执行coroutine body,程序的执行返回到caller那里
            std::suspend_never initial_suspend() {
                std::cout << "3.task-initial_suspend():do you want to susupend the current coroutine?\n";
                std::cout << "4.task-initial_suspend():don't suspend because return std::suspend_never, so continue to execute coroutine body\n";
                return {};
            }

            //也可以写成这样
            //auto initial_suspend() { return std::suspend_never{}; }

            // 调用完void return_void()或者void return_value(T v)后,就会调用final_suspend()
            // 如果final_suspend返回std::suspend_never表示不挂起协程,那么协程就会自动销毁,先后销毁promise, 协程帧上得参数和协程帧;
            // 如果返回std::suspend_always则不会自动销毁协程,需要用户手动去删除协程。
            std::suspend_never final_suspend() noexcept {
                std::cout << "15.task-final_suspend():coroutine body finished, do you want to susupend the current coroutine?\n";
                std::cout << "16.task-final_suspend():don't suspend because return std::suspend_never, and the continue will be automatically destroyed, bye\n";
                return {};
            }

            // 如果协程是void没有返回值,那么就需要定义void return_void()
            // 如果有返回值那么就定义void return_value(T v),用来保存协程的返回值
            // return_value或者return_void,这两个方法只允许存在一个
            void return_void() {
                std::cout << "14.task-return_void():coroutine don't return value, so return_void is called\n";
            }

            void unhandled_exception() {}
        };

        std::coroutine_handle<task::promise_type> handle_;
    };

    struct awaiter {
        // 调用co_wait awaiter{};时调用await_ready()
        // 表示是否准备好,要不要挂起协程
        // await_ready()返回false一般表示要挂起协程,并执行await_suspend
        // 返回true说明协程已经执行完了,这时候调用await_resume返回协程的结果。
        bool await_ready() {
            std::cout << "6.await_ready():do you want to suspend current coroutine?\n";
            std::cout << "7.await_ready():yes, suspend becase awaiter.await_ready() return false\n";
            return false;
        }

        //await_suspend 的返回值来决定是返回 caller 还是继续执行。
        //返回void:协程执行权交还给当前协程的caller。当前协程在未来某个时机被resume之后,然后执行协程函数中co_await下面的语句
        //返回true:同返回void。
        //返回false:直接执行await_resume
        void await_suspend(std::coroutine_handle<task::promise_type> handle) {
            std::cout << "8.await_suspend(std::coroutine_handle<task::promise_type> handle):execute awaiter.await_suspend()\n";
            std::thread([handle]() mutable {
                std::cout << "11.lambada():resume coroutine to execute coroutine body\n";
                handle();//等价于handle.resume();
                std::cout << "17.lambada():over\n";
            }).detach();
            std::cout << "9.await_suspend(std::coroutine_handle<task::promise_type> handle):a new thread lauched, and will return back to caller\n";
        }

        //调用完await_resume后直接执行协程函数中co_await下面的语句
        void await_resume() {
            std::cout << "12.await_resume()\n";
        }
    };

    task test() {
        std::cout << "5.test():begin to execute coroutine body, the thread id=" << std::this_thread::get_id() << ",and call co_await awaiter{};\n"; //#1
        co_await awaiter{};
        std::cout << "13.test():coroutine resumed, continue execute coroutine body now, the thread id=" << std::this_thread::get_id() << "\n"; //#3
    }

    template<typename T>
    struct lazy {
    public:
        struct promise_type;

        lazy(std::coroutine_handle<promise_type> handle) : m_handle(handle) {
            std::cout << "3.lazy(std::coroutine_handle<promise_type> handle):Construct a lazy object" << std::endl;
        }

        ~lazy() {
            std::cout << "15.~lazy():Destruct a lazy object " << std::endl;
            m_handle.destroy();
        }

        T get() {
            std::cout << "6.lazy.get():I want to execute the coroutine now. call m_handle.resume()" << std::endl;
            if (!m_handle.done()) {
                m_handle.resume();
            }
            std::cout << "13.lazy.get():We got the return value...:" << m_handle.promise().value << std::endl;
            return m_handle.promise().value;
        }

        struct promise_type {
            T value = {};

            promise_type() {
                std::cout << "1.lazy-promise_type():Promise created" << std::endl;
            }

            ~promise_type() {
                std::cout << "16.lazy- ~promise_type():Promise died" << std::endl;
            }

            auto get_return_object() {
                std::cout << "2.lazy-get_return_object():create coroutine return object, and the coroutine is created now" << std::endl;
                return lazy<T>{std::coroutine_handle<promise_type>::from_promise(*this)};
            }

            auto initial_suspend() {
                std::cout << "4.lazy-initial_suspend():Started the coroutine" << std::endl;
                return std::suspend_always{};
            }

            auto final_suspend() noexcept {
                std::cout << "12.lazy-final_suspend():Finished the coroutine" << std::endl;
                return std::suspend_always{};
            }

            void return_value(T v) {
                std::cout << "11.lazy-return_value(T v):Got coroutine result " << v << std::endl;
                value = v;
            }

            void unhandled_exception() {
                std::exit(1);
            }

            //协程体中调用co_yield xxx;的时候调用yield_value(T val)
            auto yield_value(T val) {
                std::cout << "9.lazy-yield_value(T val): " << val << std::endl;
                value = val;

                //后续不再挂起协程,继续执行
                return std::suspend_never();

//                //后续继续挂起协程
//                return std::suspend_always();
            }

        };

        std::coroutine_handle<promise_type> m_handle;
    };

    lazy<int> my_coroutine() {
        std::cout << "7.my_coroutine():Execute the coroutine function body" << std::endl;
        std::cout << "8.my_coroutine():call---co_yield 66;" << std::endl;
        co_yield 66;
        std::cout << "10.my_coroutine():call---co_return 88;" << std::endl;
        co_return 88;
    }
} // namespace Coroutine

int main() {
    Coroutine::test();
    std::cout << "10.main():come back to caller becuase of co_await awaiter\n";
    std::this_thread::sleep_for(std::chrono::seconds(1));

    std::cout << "-----------------------------------" << std::endl;

    auto coro = Coroutine::my_coroutine();
    std::cout << "5.main():call coro.get()" << std::endl;
    auto result = coro.get();
    std::cout << "14.main():The coroutine result: " << result << std::endl;

//    std::cout << "main():Second call coro.get()  " << std::endl;
//    result = coro.get();
//    std::cout << "main():The coroutine result2: " << result << std::endl;

    return 0;
}

输出结果:

1.task-promise_type():create promise object
2.task-get_return_object():create coroutine return object, and the coroutine is created now
3.task-initial_suspend():do you want to susupend the current coroutine?
4.task-initial_suspend():don't suspend because return std::suspend_never, so continue to execute coroutine body
5.test():begin to execute coroutine body, the thread id=1,and call co_await awaiter{};
6.await_ready():do you want to suspend current coroutine?
7.await_ready():yes, suspend becase awaiter.await_ready() return false
8.await_suspend(std::coroutine_handle<task::promise_type> handle):execute awaiter.await_suspend()
9.await_suspend(std::coroutine_handle<task::promise_type> handle):a new thread lauched, and will return back to caller
10.main():come back to caller becuase of co_await awaiter
11.lambada():resume coroutine to execute coroutine body
12.await_resume()
13.test():coroutine resumed, continue execute coroutine body now, the thread id=2
14.task-return_void():coroutine don't return value, so return_void is called
15.task-final_suspend():coroutine body finished, do you want to susupend the current coroutine?
16.task-final_suspend():don't suspend because return std::suspend_never, and the continue will be automatically destroyed, bye
17.lambada():over
-----------------------------------
1.lazy-promise_type():Promise created
2.lazy-get_return_object():create coroutine return object, and the coroutine is created now
3.lazy(std::coroutine_handle<promise_type> handle):Construct a lazy object
4.lazy-initial_suspend():Started the coroutine
5.main():call coro.get()
6.lazy.get():I want to execute the coroutine now. call m_handle.resume()
7.my_coroutine():Execute the coroutine function body
8.my_coroutine():call---co_yield 66;
9.lazy-yield_value(T val): 66
10.my_coroutine():call---co_return 88;
11.lazy-return_value(T v):Got coroutine result 88
12.lazy-final_suspend():Finished the coroutine
13.lazy.get():We got the return value...:88
14.main():The coroutine result: 88
15.~lazy():Destruct a lazy object 
16.lazy- ~promise_type():Promise died

Process finished with exit code 0

参考:
1.https://blog.csdn.net/csdnnews/article/details/124123024
2.http://purecpp.org/detail?id=2270
3.http://purecpp.org/detail?id=2278
4.http://purecpp.org/detail?id=2275

posted @ 2022-04-16 16:24  DarkH  阅读(1962)  评论(0编辑  收藏  举报