C ++ Coroutine的心理模型
A Mental Model for C++ Coroutine

原始链接: https://uvdn7.github.io/cpp-coro/

C ++ Coroutines不是一个预建的库,而是一项规范,要求图书馆作者实施自定义点,以供呼叫,返回,暂停,恢复和销毁操作。与Rust的“ Future”性状不同,C ++具有更大的灵活性。自定义以返回类型`task'task `开头,该`必须定义promise_type`指定在呼叫,返回和破坏过程中指定行为。 悬架发生在`ination_suspend`,`final_suspend`,`co_await''和`co_yield“。 `co_await`使用“等待物”和“等待者”。 编译器创建了一个“等待者”对象,在“等待”上调用`oterator co_await'。等待者的“ agat_suspend”获得了当前的Coroutine手柄,在解决等待的问题时可以恢复Coroutine(例如,`co_sleep`完成了)。 `eawait_resume`返回`co_await'表达式的值。 `final_suspend`还返回等待者,允许在Coroutine结束之前进行自定义。 ```eawait_transform'''点可以将co _await中的coroutine包裹表达式变成等待。这种灵活性允许对Coroutine行为进行细粒度的控制。

该黑客新闻线程讨论了有关C ++ Coroutines和C ++标准添加的博客文章。一个关键点是在C ++ 26中可能包含默认异步运行时,类似于C#的方法。尽管这提供了便利,但有些人认为与Rust的模型相比,它可能会限制灵活性和创新,该模型使开发人员可以从多个异步运行时间中进行选择。 对话还涉及C ++ Coroutines的复杂性,将它们与更简单的Coroutines进行了对比。评论者讨论了理解和实施它们的困难,称赞像雷蒙德·陈(Raymond Chen)的系列之类的资源,但承认学习曲线。有些人突出了Coroutines对网络等任务的好处,与传统的回调或线程方法相比,它们可以简化代码并提高效率。其他人则深入研究实施类似光纤功能和潜在编译器优化的技术细节。最后,关于作者(UVDN7)的错别字是真正人类作者身份的标志,有一种轻松的交流。
相关文章

原文

C++ coroutine is not a library that is ready to go (e.g. std::vector). It is not even a trait (think of Rust’s Future trait) that library writers or users can implement (or the compiler generates for you in the case of Rust). C++ coroutine is a specification that defines a set of customization points, that requires library writers to implement in order to get a functional coroutine.

A function supports two operations - call and return. A corotuine (in any language) is a generalization of a function. It supports suspend, resume, and destroy in addition of call and return . By this definition, C++ coroutine, Rust Future, are both coroutines. C++ coroutine provides customization points around call, return, suspend, and resume; it exposes a handle that allows users to explicitly destroy a coroutine as well.

Here is what a potential coroutine add could look like. All it does is sleep one second, add two numbers and return the result. add should suspend at co_await and resume execution once co_sleep(1) completes.

Task<int32_t> add(int32_t a, int32_t b) {
    uint32_t actualSleepSec = co_await co_sleep(1);
    co_return a + b;
}

Task<uint32_t> co_sleep(uint32_t sec) {
    // ...
}

Similar to many other languages, the presence of the keyword co_await signals that add is a coroutine. Let us pretend to be a compiler for a second, the only thing that can be used to customize the behavior of the coroutine is really just the return type Task<T>. And indeed that is where C++ extracts the promise_type to start the customization process. We will cover more about the promise_type later in the post.

The Obvious/Rust Approach

An obvious option here is to make Task<T> a subclass of some language defined interface (which owns the coroutine frame for storing state across co_await points), where a set of customization points can be implemented as member functions to specify the behavior of this coroutine. If we lump everything into a single class (Task<T> in this case), resume, will be part of the public interface as well (usually suspend only takes place at well defined await points), as one need to be able to resume the coroutine somehow, just like Rust’s Future::poll. How does a coroutine know it is ready to be resumed? In the Rust’s case, it essentially passes a callback to the Future. The callback will resume (poll) the Future when its dependency is satisfied. In our example, the callback will resume the add coroutine after co_sleep is complete. But it still needs to somehow pass the return value from co_sleep to the caller. Rust solves this problem by passing the callback all the way from the top-most Future to the bottom Future, and whenever the state changes (because e.g. a network reply is received), it invokes the callback to reschedule the entire Future chain. In our example, the return value from co_await co_sleep(1); will be acquired on the next poll on add. This is a good way of modeling async computation. C++’s coroutine spec, which we will see in a minute is way more flexible. Actually it is so flexible that we can implement Rust Future’s poll-based semantics (barring the compile transformation of each Future’s state machine) with C++ coroutine.

C++ Coroutine philosophy

Flexibility - it allows one to customize every coroutine operation in meaningful ways. Most importantly,

  • what happens when a coroutien is first called
  • what happens before a coroutine returns a value
  • what happens right before a coroutine reaches the enf of its lifetime
  • when one wants to destroy a coroutine
  • what happens after a coroutine is suspended
  • what happens before a coroutine is resumed

So each coroutine can have its own unique behaviors.

The promise_type

As we observed earlier, the return type Task<T> is the best entry-point for users to customize coroutine behaviors. C++ coroutine requires a Task<T>::promise_type type be defined.

  1. the promise type can specify the coroutine behavior - most importantly when add is called, before add returns, before the end of the coroutine’s lifetime (destroy)
  2. the promise object (of the promise type) can be used to store additional state in the coroutine frame

When a coroutine is first called, C++ will automatically create a promise object for you, which lives inside the coroutine frame (that is allocated at the same time).

The Awaiter and the Awaitable

The promise_type covers the customization for call, return operation of a coroutine. We have suspend and resume left. Suspension takes place at well defined suspension points - initial_suspend(right after call), final_suspend(right before the end), co_await, co_yield. If one simply calls add(1, 2) with initial_suspend set to suspend_always, the coroutine is suspended. In most cases, one wants to customize the behavior of suspend so that the suspended coroutine can be resumeed later. A coroutine can be suspended for two main reasons

  1. to provide a customization opportunity - e.g. initial_suspend and final_suspend
  2. to wait for a dependency - a.k.a awaitable

In the first case, there is no dependency/awaitable. In the second case, it would be very helpful to have reference to the awaitable when customizing the suspend operation, so that we can resume the current coroutine when the awaitable is resolved. It would be kind of awkward to provide a suspend customization point that provides an awaitable sometimes. C++ coroutine solves this problem by introducing the awaitor concept. At co_await awaitable;, the compiler will create an awaiter object via calling the operator co_await override on the awaitable (let us ignore the await_transform for now). In most cases, the awaitable will be a data member of the awaitor object. Magic methods await_suspend, and await_resume on the awaiter object will be used to customize the suspend and resume operation of the coroutine. Most importantly, await_suspend will be called with an argument that represents the current coroutine, which is just suspended. So at this point, the Awaitor has reference of the awaitable, as well as the current coroutine. It allows us to set up when to call the resume operation. A common approach is to store the current coroutine handle in the awaitable’s promise object; when the awaitable reaches its end of lifetime (final_suspend), it can retrieve the awaiting coroutine and resume it. await_resume’s return value will be the value of the co_await expression. Usually the result can be passed via the awaitable’s promise object as well.

final_suspend returns an awaiter object. We have seen Awaitors already - suspend_always is an empty awaiter type that has await_ready returns false always. It is conceptually equivalent to putting co_await final_suspend(); right before the end. await_suspend can be customized for the final awaiter object, which will again have a handle to the current coroutine, where we can extract the promise object with whatever state we put in.

We are making a big assumption here that every awaitable itself is also a coroutine. It is likely true most of the time. To make it true all the time, it is where await_transform comes in. It provides yet another customization point. It allows the coroutine to transform (usually wrap) the expression in co_await expr; into an awaitable.

联系我们 contact @ memedata.com