C++20 中协程的机制是什么

What are the mechanics of coroutines in C++20?

本文关键字:机制 是什么 C++20      更新时间:2023-10-16

我试图阅读文档(cpp首选项和功能本身的标准文档),这些文档是关于在调用、挂起、恢复和终止协程函数时调用的操作序列。 该文档深入概述了各种扩展点,这些扩展点允许库开发人员使用库组件自定义其协程的行为。 在高层次上,这种语言功能似乎经过深思熟虑。

不幸的是,我很难遵循协程执行的机制,以及我作为库开发人员如何使用各种扩展点来自定义所述协程的执行。甚至从哪里开始。

以下功能位于我不完全理解的新自定义点集中:

  • initial_suspend()
  • return_void()
  • return_value()
  • await_ready()
  • await_suspend()
  • await_resume()
  • final_suspend()
  • unhandled_exception()

有人可以用高级伪代码描述编译器在运行用户协程时生成的代码吗? 在抽象的层面上,我试图弄清楚何时调用await_suspendawait_resumeawait_readyawait_transformreturn_value等函数,它们的作用是什么,以及我如何使用它们来编写协程库。


不确定这是否是题外话,但这里的一些介绍性资源对整个社区非常有帮助。 像在 cppcoro 中一样在谷歌上搜索并深入研究库实现并不能帮助我克服这个最初的障碍:(

N4775 概述了 C++20 的协程建议。它引入了许多不同的想法。以下是我在 https://dwcomputersolutions.net 的博客。更多信息可以在我的其他帖子中找到。

在我们检查整个 Hello World 协程程序之前,请先了解一下 各个部分逐步完成。其中包括:

  1. 协程承诺
  2. 协程上下文
  3. 协程未来
  4. 协程句柄
  5. 协程本身
  6. 实际使用协程的子例程

整个文件都包含在此末尾 发布。

协程

Future f()
{
co_return 42;
}

我们实例化我们的协程

Future myFuture = f();

这是一个简单的协程,只返回值42。它是一个协程 因为它包含关键字co_return.任何具有关键字的函数co_awaitco_returnco_yield是一个协程。

您会注意到的第一件事是,尽管我们返回了一个整数, 协程返回类型为(用户定义的)类型"未来"。原因是 当我们调用协程时,我们现在不运行函数,而是运行 初始化一个对象,最终将获得我们正在寻找的值 又名我们的未来。

查找承诺的类型

当我们实例化协程时,编译器做的第一件事就是找到 表示此特定类型的协程的 promise 类型。

我们告诉编译器什么承诺类型属于什么协程函数 通过创建模板部分专用化进行签名

template <typename R, typename P...>
struct coroutine_trait
{};

与定义我们承诺类型的名为promise_type的成员

对于我们的示例,我们可能希望使用类似以下内容:

template<>
struct std::experimental::coroutines_v1::coroutine_traits<Future> {
using promise_type = Promise;
};

在这里,我们创建一个专门化coroutine_trait不指定参数和 返回类型Future,这与我们的协程函数签名完全匹配Future f(void).promise_type是承诺类型,在我们的例子中是struct Promise.

现在是用户,我们通常不会创建自己的coroutine_trait专用化,因为协程库提供了一种很好的简单方法 在 Future 类本身中指定promise_type。稍后会详细介绍。

协程上下文

正如我在上一篇文章中提到的,因为协程是可挂起的,并且 可恢复的局部变量不能始终存储在堆栈中。要存储 非堆栈安全的局部变量,编译器将在 堆。我们的承诺实例也将存储。

承诺、未来和把手

协程大多是无用的,除非它们能够与 外面的世界。我们的承诺告诉我们,当我们的 Future 对象允许其他代码与协程交互。承诺和 然后,将来通过我们的协程句柄相互通信。

承诺

一个简单的协程承诺如下所示:

struct Promise 
{
Promise() : val (-1), done (false) {}
std::experimental::coroutines_v1::suspend_never initial_suspend() { return {}; }
std::experimental::coroutines_v1::suspend_always final_suspend() {
this->done = true;
return {}; 
}
Future get_return_object();
void unhandled_exception() { abort(); }
void return_value(int val) {
this->val = val;
}

int val;
bool done;    
};
Future Promise::get_return_object()
{
return Future { Handle::from_promise(*this) };
}

如前所述,承诺是在实例化协程时分配的,并且 在协程的整个生存期内退出。

完成后,编译器调用get_return_object此用户定义的函数是 然后负责创建 Future 对象并将其返回给 协程初始化器。

在我们的例子中,我们希望我们的未来能够与我们的协程进行通信。 因此,我们使用协程的句柄创建我们的未来。这将使我们的 未来访问我们的承诺。

创建协程后,我们需要知道是否要开始运行 它立即或我们是否希望它立即保持暂停状态。这是 通过调用Promise::initial_suspend()函数完成。此函数返回 一个等待者,我们将在另一篇文章中查看。

在我们的例子中,由于我们确实希望函数立即启动,我们调用suspend_never.如果我们暂停了该功能,我们需要启动 通过调用句柄上的 Resume 方法进行协程。

我们需要知道当调用co_return运算符时该怎么做 协程。这是通过return_value功能完成的。在这种情况下,我们 将值存储在承诺中,以便以后通过将来检索。

如果发生异常,我们需要知道该怎么做。这是由unhandled_exception功能。由于在我们的示例中,异常不应 发生,我们只是中止。

最后,我们需要知道在销毁协程之前该怎么做。这是 通过final_suspend function完成 在这种情况下,由于我们想检索 结果所以我们返回suspend_always.然后必须销毁协程 通过协程句柄destroy方法。否则,如果我们返回suspend_never协程在完成运行后立即自行销毁。

手柄

句柄允许访问协程及其承诺。有两个 味道,当我们不需要访问承诺和 与承诺类型的协程句柄,用于何时需要访问承诺。

template <typename _Promise = void>
class coroutine_handle;
template <>
class coroutine_handle<void> {
public:
void operator()() { resume(); }
//resumes a suspended coroutine
void resume();
//destroys a suspended coroutine
void destroy();
//determines whether the coroutine is finished
bool done() const;
};
template <Promise>
class coroutine_handle : public coroutine_handle<void>
{
//gets the promise from the handle
Promise& promise() const;
//gets the handle from the promise
static coroutine_handle from_promise(Promise& promise) no_except;
};

未来

未来是这样的:

class [[nodiscard]] Future
{
public:
explicit Future(Handle handle)
: m_handle (handle) 
{}
~Future() {
if (m_handle) {
m_handle.destroy();
}
}
using promise_type = Promise;
int operator()();
private:
Handle m_handle;    
};
int Future::operator()()
{
if (m_handle && m_handle.promise().done) {
return m_handle.promise().val;
} else {
return -1;
}
}

Future 对象负责将协程抽象到外部 世界。我们有一个构造函数,它根据 承诺的get_return_object实施。

析构函数破坏协程,因为在我们的例子中,它是未来 控制是承诺的一生。

最后我们有一行:

using promise_type = Promise;

C++库使我们不必像以前那样实现自己的coroutine_trait上面,如果我们在协程的 return 类中定义我们的promise_type

我们有它。我们的第一个简单协程。

完整来源


#include <experimental/coroutine>
#include <iostream>
struct Promise;
class Future;
using Handle = std::experimental::coroutines_v1::coroutine_handle<Promise>;
struct Promise 
{
Promise() : val (-1), done (false) {}
std::experimental::coroutines_v1::suspend_never initial_suspend() { return {}; }
std::experimental::coroutines_v1::suspend_always final_suspend() {
this->done = true;
return {}; 
}
Future get_return_object();
void unhandled_exception() { abort(); }
void return_value(int val) {
this->val = val;
}

int val;
bool done;    
};
class [[nodiscard]] Future
{
public:
explicit Future(Handle handle)
: m_handle (handle) 
{}
~Future() {
if (m_handle) {
m_handle.destroy();
}
}
using promise_type = Promise;
int operator()();
private:
Handle m_handle;    
};
Future Promise::get_return_object()
{
return Future { Handle::from_promise(*this) };
}

int Future::operator()()
{
if (m_handle && m_handle.promise().done) {
return m_handle.promise().val;
} else {
return -1;
}
}
//The Co-routine
Future f()
{
co_return 42;
}
int main()
{
Future myFuture = f();
std::cout << "The value of myFuture is " << myFuture() << std::endl;
return 0;
}

##Awaiters

co_await运算符允许我们暂停协程并返回控制 返回到协程调用方。这使我们能够在等待操作完成的同时执行其他工作。当它们完成时,我们可以从 正是我们离开的地方。

co_await运算符可通过多种方式处理表达式 在其右侧。现在,我们将考虑最简单的情况,这就是我们的co_await表达式返回一个等待者。

等待程序是实现以下内容的简单structclass方法:await_readyawait_suspendawait_resume

bool await_ready() const {...}只是返回我们是否准备好恢复我们的 协程,或者我们是否需要考虑暂停协程。若await_ready返回假。我们继续运行await_suspend

await_suspend方法有多个签名可用。最简单的是void await_suspend(coroutine_handle<> handle) {...}.这是手柄 我们的co_await将挂起的协程对象。此功能完成后, 控制权将返回到协程对象的调用方。就是这个功能 负责存储协程句柄以供以后使用,以便我们的 协程不会永远处于挂起状态。

一旦handle.resume()被调用;await_ready返回假;或其他一些 机制恢复我们的协程,调用auto await_resume()的方法。这 从await_resume返回值是co_await运算符返回的值。 有时exprco_await expr返回等待者是不切实际的 如上所述。如果expr返回一个类,该类可以提供自己的类 将返回等待者的Awaiter operator co_await (...)实例。 或者,可以在我们的promise_type中实现一个await_transform方法,它将expr转换为 Awaiter。

现在我们已经描述了 Awaiter,我想指出initial_suspendfinal_suspend方法在我们的promise_type都返回 等待者。对象suspend_alwayssuspend_never是微不足道的等待者。suspend_always返回 true 到await_readysuspend_never返回 假。不过,没有什么能阻止您推出自己的产品。

如果你好奇现实生活中的等待者是什么样子的,看看我的 未来对象。 它将协程句柄存储在 lamda 中以供以后处理。