文章目录
- 前言
- 什么是协程
- 协程实现原理
- C++协程的最小例子
- 1
- 2
- 3
- 4
- 5
- 协程等效代码
- 协程传值的例子
前言
最近学习了一下C++协程,这篇文章将介绍协程的相关概念,以及在C++中如何使用协程。
什么是协程
C++中,协程(coroutines)可以理解为一个可以暂停和恢复执行的函数。什么意思呢?例如有以下协程函数:
Task taskFunc()
{...co_await doSomething(); // 1doSomething2(); // 2
}int main() {auto task = taskFunc(); // 3... // 4task.resume(); // 5
}
3处调用协程函数,在执行到1的时候(例如doSomething()异步请求网络资源),函数暂停执行,代码走到4处,等后面某个合适的时机5,恢复执行协程函数,函数从2继续执行,对数据进行处理,执行顺序是3->1->4->5->2,在这个过程中,执行的线程始终是同一个(除非有意让协程在不同线程中切换执行)。
协程实现原理
C++中,协程是通过编译器来实现的,编译器根据代码中的co_return/co_await关键字,识别一个函数为协程函数,协程函数的返回值类型有如下要求:返回值是一个类,类中包含一个名称为promise_type的类型(必须是这个类名),promise_type必须实现几个函数:
- get_return_object
- initial_suspend
- final_suspend
- unhandled_exception
- return_void或者return_value
通常来说,这个外层的类会有一个std::coroutine_handle\<promise_type\> handle
成员保存协程的句柄,用于控制协程和获取协程的相关数据。编译器会将协程状态和协程中的数据保存在堆上,并生成执行协程所需的代码。
C++协程的最小例子
#include <iostream>
#include <coroutine>template <bool READY>
struct Awaiter {bool await_ready() noexcept {std::cout << "await_ready: " << READY << std::endl;return READY;}void await_resume() noexcept {std::cout << "await_resume" << std::endl;}void await_suspend(std::coroutine_handle<>) noexcept {std::cout << "await_suspend" << std::endl;}
};struct TaskPromise {struct promise_type {TaskPromise get_return_object() {std::cout << "get_return_object" << std::endl;return TaskPromise{std::coroutine_handle<promise_type>::from_promise(*this)};}Awaiter<true> initial_suspend() noexcept {std::cout << "initial_suspend" << std::endl;return {};}Awaiter<true> final_suspend() noexcept {std::cout << "final_suspend" << std::endl;return {};}void unhandled_exception() {std::cout << "unhandled_exception" << std::endl;}void return_void() noexcept {std::cout << "return_void" << std::endl;}};void resume() {std::cout << "resume" << std::endl;handle.resume();}std::coroutine_handle<promise_type> handle;
};TaskPromise task_func() {std::cout << "task first run" << std::endl;co_await Awaiter<false>{};std::cout << "task resume" << std::endl;
}int main() {auto promise = task_func(); // 1promise.resume();return 0;
}
接下来我们一步一步解析协程的执行过程:
1
首先task_func就是如前所述的协程函数,因为它包含了一个co_await关键字,返回值是TaskPromise类型,其包含了一个promist_type类型,实现了一些必须的函数,满足协程的所有要求。
当一个协程被调用时,编译器不会像普通函数一样在栈上分配空间。相反,它会在堆上动态分配一块内存,这块内存被称为“协程帧”。这个帧里包含了:
- 协程的 Promise 对象 (promise_type)
- 所有按值传递的参数的拷贝
- 所有在 co_await 挂起点之间需要保持状态的局部变量
- 协程当前执行到的位置(状态机的状态)
总结来说就是,第一次执行协程函数task_func时,编译器会在堆上创建一个协程帧用来保存协程状态和局部变量等信息。接着,调用promise_type的get_return_object方法,该函数的返回值必须和协程函数的返回值一致,在task_func暂停/结束执行时,返回值就是get_return_object的返回值。
get_return_object函数的实现通常是返回外层类对象,并使用promise_type对象初始化coroutine_handle:
std::coroutine_handle<promise_type>::from_promise(*this)
std::coroutine_handle可以理解为类似std::thread的类,是一个wrapper,用来控制协程和获取协程数据的。from_promise将一个promise_type类型转换为std::coroutine_handle,相应的std::coroutine_handle::promise方法用于获取handle中的promise_type对象。
2
接着,执行的是initial_suspend,这个函数的返回值是一个类类型,通常称为awaiter,这个类用于控制接下来要继续执行协程函数,还是暂停执行,这个类必须实现如下函数:await_ready,await_suspend,await_resume,关于这三个函数的详细解释,我们等到co_await时再说。
initial_suspend返回类型可以使用stl提供的两种awaiter:std::suspend_always和std::suspend_never,分别表示总是挂起或者总是继续执行。其实现也很简单:
// STRUCT suspend_always
struct suspend_always {_NODISCARD constexpr bool await_ready() const noexcept {return false;}constexpr void await_suspend(coroutine_handle<>) const noexcept {}constexpr void await_resume() const noexcept {}
};
这样就不用自己实现awaiter了,在我们的例子中,我们实现了自己的awaiter类,并且await_ready返回true,此时会立即调用await_resume。
3
接着开始真正执行task_func函数的第一行
std::cout << "task first run" << std::endl;
然后我们遇到了co_await表达式,co_await后面跟着的是awaiter对象,我们详细的介绍一下awaiter的三个函数:
- await_ready。没有参数,返回bool类型,表示接下来应该继续执行(true)还是暂停执行(false)
- await_suspend。当await_ready返回false时,表示没有准备好数据,此时下一个执行的函数就是await_suspend。await_suspend参数是协程句柄类型std::coroutine_handle,这个参数是编译器帮忙传入的。await_suspend返回值可以是void,也可以是bool(如果返回false则又会继续执行协程),甚至可以是其他协程句柄,从而执行其他协程(这是高级话题,我们以后再说)
- await_resume。当await_ready返回true时,表示已经准备好数据,此时下一个执行的函数就是await_resume。await_resume没有参数,返回值可以是任意类型,这个返回值会作为co_return表达式的返回值。
在我们的例子中,co_await Awaiter<false>{};
会让协程挂起,于是编译器将当前表达式生成的awaiter对象保存在协程帧中,然后协程函数task_func返回一个TaskPromise对象(由前面所说的get_return_object构造),回到了main中执行。
4
接着我们调用了TaskPromise::resume,这个函数只做了一件事,就是调用coroutine_handle的resume方法,它会调用之前co_wait挂起时保存的awaiter对象的resume方法,然后继续执行协程函数:
std::cout << "task resume" << std::endl;
5
最后,协程函数结束执行,我们没有写co_return,编译器会默认补上co_return在最后,co_return会调用promise_type::return_void()函数,表示没有返回值。如果有返回值,就需要定义一个叫void return_value(T t)
的函数,return_value和return_void不能共存。
接着编译器调用promise_type::final_suspend结束协程,final_suspend类似initial_suspend,如果挂起,协程不会立即销毁内部的状态信息,反之则会立即销毁,因为我们可能还有部分信息存在promise_type对象中,所以在finial_suspend挂起后,则需要手动释放coroutine_handle的资源,可以采用RAII的方式,在外层类中的析构函数释放coroutine_handle:
~TaskPromise()
{handle.destroy();
}
如果发生异常,则会调用promise::unhandled_exception。
协程等效代码
综上,我们可以写出task_func协程执行过程的伪代码:
TaskPromise task_func() {// No parameters and local variables.auto state = new __TaskPromise_state_(); // has TaskPromise::promise_type promise; TaskPromise coro = state.promise.get_return_object();try {co_await p.inital_suspend();std::cout << "task first run" << std::endl;co_await Awaiter<false>{};std::cout << "task resume" << std::endl;} catch (...) {state.promise.unhandled_exception();}co_await state.promise.final_suspend();
}
协程传值的例子
下面是一个模拟通过协程获取数据,最终返回在main中取数据的例子
#include <iostream>
#include <coroutine>
#include <future>
#include <thread>struct TaskPromise {struct promise_type {TaskPromise get_return_object() {std::cout << "get_return_object(), thread_id: " << std::this_thread::get_id() << std::endl;return TaskPromise{std::coroutine_handle<promise_type>::from_promise(*this)};}std::suspend_always initial_suspend() noexcept { return {}; }std::suspend_always final_suspend() noexcept { return {}; }void unhandled_exception() {}void return_void() noexcept {}size_t data = 0;};std::coroutine_handle<promise_type> handle;
};struct Awaiter {bool await_ready() noexcept {std::cout << "await_ready(), thread_id: " << std::this_thread::get_id() << std::endl;return false;}void await_suspend(std::coroutine_handle<TaskPromise::promise_type> handle) noexcept {std::cout << "await_suspend(), thread_id: " << std::this_thread::get_id() << std::endl;auto thread = std::thread([=]() {std::this_thread::sleep_for(std::chrono::seconds(1));handle.promise().data = 1;handle.resume();});thread.join();}void await_resume() noexcept {std::cout << "await_resume(), thread_id: " << std::this_thread::get_id() << std::endl;}
};TaskPromise task_func() {std::cout << "task_func() step 1, thread_id: " << std::this_thread::get_id() << std::endl;co_await Awaiter{};std::cout << "task_func() step 2, thread_id: " << std::this_thread::get_id() << std::endl;
}int main() {std::cout << "main(), thread_id: " << std::this_thread::get_id() << std::endl;auto promise = task_func();std::cout << "main(), data: " << promise.handle.promise().data << ", thread_id: " << std::this_thread::get_id() << std::endl;promise.handle.resume();std::cout << "main(), data: " << promise.handle.promise().data << ", thread_id: " << std::this_thread::get_id() << std::endl;return 0;
}
参考:1. https://mp.weixin.qq.com/s/0njDHtz_SGPkrr4ndAWHaA