【协程】C++20协程初体验
为什么我们需要协程?
为什么我们有了线程还需要协程呢?(其实这个问题不应该这么问,协程的出现在线程之前)在一个进程中虽然我们可以创建多个线程,但是在一个进程中能创建的线程数量是有限制的,并且线程的调度仍然受操作系统控制,也就是说线程何时抢占、何时被抢占对于开发者来说都是透明的,并且在调度的过程中还可能涉及到用户态和内核态的切换开销。
当我们需要去处理一个非常耗时的IO操作时(假设使用的阻塞IO),为了不阻塞当前线程,我们可能会想到新建一个线程去执行这个操作,但是线程的创建和调度也是需要消耗资源的,我们希望有更加轻便的方法,例如:在当前线程中,一个任务遇到阻塞IO时,不要傻傻的停在这里,而是暂停这个任务,转而去执行其他任务,直到IO完成再恢复之前的任务来运行。
这里看起来是不是像两个函数之间的调用和被调用关系?确实有点像,但区别可大了,在任务1中我们并没有去显式地调用任务2!
协程的最本质的解释是“可以挂起和恢复的函数”。例如上图中我们遇到耗时的IO操作了,我们就可以主动将当前运行的函数挂起(suspend),让其等待(await)IO操作,让线程去运行其他的函数,直到IO操作完成后再恢复(resume)。上下文切换的时机是靠调用方(写代码的开发人员)自身去控制的,这样协程的调度掌握在我们自己手中,相比与线程减小了系统切换上下文和其他资源的开销。因此协程在需要处理大量I/O操作或者并发任务的情况下提高程序的性能和可维护性。
C++20带来的协程
C20带来的协程并不像python或lua中的那么易用,相反,C给我们提供的是更为底层的操作(不过C++23已经有std::generator
这种更高级的抽象,之后也会有更多丰富的用法)。
C++协程中有很重要的三个概念:
Promise
Awaitable
Coroutine Handle
Promise
promise_type
是每个协程函数的幕后执行对象。它主要负责以下几个任务:
- 创建和初始化协程:当协程开始执行时,编译器会通过
promise_type
创建一个对象,并调用其get_return_object()
方法来获取协程的返回对象。 - 处理协程的暂停和恢复:协程在暂停时会调用
yield_value()
或await_suspend()
等方法来处理协程的状态,并决定何时恢复。 - 处理协程的结束:当协程执行结束时,
return_void()
或return_value()
会被调用,来处理协程的返回结果。
以下是 promise_type
中一些常见的方法:
get_return_object()
:用于创建和返回协程的返回对象,一般是协程返回类型的实例。initial_suspend()
:返回一个std::suspend_always
或std::suspend_never
,决定协程在启动时是否立即暂停。final_suspend()
:返回一个std::suspend_always
或std::suspend_never
,决定协程在结束时是否暂停,以允许调用方执行清理操作。return_void()
或return_value(T value)
:用于在协程完成时返回结果。return_void()
用于没有返回值的协程,而return_value(T)
则用于有返回值的协程。yield_value(T value)
:用于生成值并让协程暂停,等待下一次恢复时继续执行。
Awaitable
Awaitable
是一个可以与 co_await
表达式一起使用的对象或类型。Awaitable
对象必须提供一组特定的方法,使协程可以暂停执行,并在某个条件满足时继续执行。
co_await
是 C++ 协程中的一种操作符,用于暂停协程并等待某个条件的满足。当协程遇到 co_await
时,它会暂停,并返回控制权给调用者。协程可以通过调用 co_await some_awaitable
来等待 some_awaitable
完成。
一个 Awaitable
对象需要提供以下三个方法中的一个或多个:
operator co_await
:返回一个Awaitable
对象。Awaitable
对象是实际实现等待逻辑的对象。await_ready()
:这是Awaitable
对象上的方法。它返回一个bool
,用于指示是否需要等待。如果返回true
,协程将不会暂停。await_suspend(std::coroutine_handle<>)
:这是Awaitable
对象上的方法。它接受一个std::coroutine_handle<>
参数,并在协程暂停时调用。这个方法决定协程何时恢复执行。await_resume()
:这是Awaitable
对象上的方法。它在协程恢复时调用,并返回co_await
表达式的结果。
C++中提供了两个简单的Awaitable:std::suspend_never
和std::suspend_always
。
Coroutine Handle
std::coroutine_handle
是 C++20 协程库中一个核心的工具类,用于表示和操作协程。它是一个模板类,通常用来指向协程的状态信息。它可以通过协程的 promise_type
访问和控制协程的状态。每个协程在创建时,都会生成一个 std::coroutine_handle
,用于管理协程的生命周期。
std::coroutine_handle
提供了一系列方法来控制协程的执行,包括以下几个主要功能:
- 创建和获取句柄:
std::coroutine_handle<>::from_address(void* ptr)
:通过指针获取一个句柄。std::coroutine_handle<promise_type>::from_promise(promise_type& promise)
:通过promise_type
对象创建一个句柄。
- 控制协程的执行:
void resume()
:恢复协程的执行。void destroy()
:销毁协程并释放其占用的资源。void operator()()
:等效于resume()
,恢复协程的执行。void* address()
:返回协程句柄的地址,用于低级操作。
- 检查协程的状态:
bool done()
:检查协程是否已经完成执行。
- 访问
promise_type
:promise_type& promise()
:获取与当前协程关联的promise_type
对象,允许访问协程内部状态。
简单示例
这里有一个使用C++20 coroutine来实现挂起和恢复函数的例子。
1 |
|
在main函数中,一开始我们启动了协程hello
,输出了“Hello”后被挂起;之后启动了协程hello2
,其输出“你好\nhello two two\n”后也被挂起。紧接着我们通过coroutine_handle来依次恢复这两个协程,最终输出结果为:
1 | Hello |
References
- https://zplutor.github.io/2022/03/25/cpp-coroutine-beginner/
- https://www.bluepuni.com/archives/stackless-coroutine-and-asio-coroutine
- https://zhuanlan.zhihu.com/p/355100152?utm_psn=1808059511308697600
- https://itnext.io/c-20-coroutines-complete-guide-7c3fc08db89d
- https://jasonkayzk.github.io/2022/06/03/%E6%B5%85%E8%B0%88%E5%8D%8F%E7%A8%8B/
- https://lewissbaker.github.io/2017/11/17/understanding-operator-co-await
- https://juejin.cn/post/6844903715099377672