使用上下文调度协程

Scheduling a coroutine with a context

有很多教程解释了如何在 C++ 中轻松使用协程,但我花了很多时间来了解如何安排“分离的”协程。 假设,我有以下协程结果类型的定义:

struct task {
  struct promise_type {
    auto initial_suspend() const noexcept { return std::suspend_never{}; }
    auto final_suspend() const noexcept { return std::suspend_never{}; }
    void return_void() const noexcept { }
    void unhandled_exception() const { std::terminate(); }
    task get_return_object() const noexcept { return {}; }
  };
};

还有一种运行“分离”协程的方法,即异步运行它。

/// Handler should have overloaded operator() returning task.
template<class Handler>
void schedule_coroutine(Handler &&handler) {
  std::thread([handler = std::forward<Handler>(handler)]() { handler(); }).detach();
}

显然,我不能将 lambda 函数或任何其他具有状态的函数对象传递给此方法,因为一旦协程被挂起,传递给 std::thread 方法的 lambda 将与所有捕获的变量。

task coroutine_1() {
  std::vector<object> objects;
  // ...
  schedule_coroutine([objects]() -> task {
     // ...
     co_await something;
     // ...
     co_return;
  });

  // ...
  co_return;
}

int main() {
  // ...
  schedule_coroutine(coroutine_1);
  // ...
}

我认为应该有一种方法可以以某种方式保存 handler(最好在协程承诺附近或内部),以便下次恢复协程时它不会尝试访问已销毁的对象数据。但不幸的是我不知道该怎么做。

希望我没听错,但我认为这里可能存在一些误解。首先,你显然不能分离协程,那根本没有任何意义。但是你可以肯定地在协程中执行异步任务,即使在我看来这完全违背了它的目的。

但是让我们看一下您发布的第二个代码块。在这里您调用 std::async 并将处理程序转发给它。现在,为了防止任何类型的早期破坏,您应该改用 std::move 并将处理程序传递给 lambda,这样只要 lambda 函数的范围有效,它就会保持活动状态。这应该也已经回答了您的最后一个问题,因为您希望存储此处理程序的地方将是 lambda 捕获本身。

另一件困扰我的事情是 std::async 的用法。该调用将 return 一种 std::future 类型,它将阻塞直到 lambda 被执行。但这只有在您将启动类型设置为 std::launch::async 时才会发生,否则您以后需要调用 .get().wait(),因为默认启动类型是 std::launch::deferred 并且这只会延迟触发(意思是:当你实际请求结果时)。

因此,在您的情况下,如果您真的想以这种方式使用协程,我建议改用 std::thread 并将其存储在伪全局的某个地方以备后用 join()。但同样,我认为您不会真的想以这种方式使用协程机制。

我认为您的问题是对 co_await 协程的工作方式的普遍(且普遍)误解。

当函数执行 co_await <expr> 时,这(通常)意味着函数暂停执行,直到 expr 恢复执行。也就是说,您的函数正在等待某个进程完成(通常 returns 一个值)。由 expr 表示的那个进程是应该恢复功能的进程(通常)。

这样做的重点是让异步执行的代码尽可能地看起来像同步代码。在同步代码中,您会执行类似 <expr>.wait() 的操作,其中 wait 是一个等待 expr 表示的任务完成的函数。您不是“等待”它,而是“等待”或“异步等待”它。函数的其余部分相对于调用者异步执行,具体取决于 expr 何时完成以及它如何决定恢复函数的执行。这样一来,co_await <expr> 看起来和表现起来就很像 <expr>.wait()

Compiler Magictm 然后进入幕后使其异步。

因此,启动“分离协程”的想法在此框架内没有意义。协程函数的调用者(通常)不是决定协程执行位置的人;这是协程在执行期间调用的进程决定的。

您的 schedule_coroutine 函数实际上应该只是一个常规的“异步执行函数”操作。它不应该与协程有任何特定的关联,也不应该期望给定的仿函数是或代表某个异步任务,或者如果它恰好调用 co_await。该函数只是要创建一个新线程并在其上执行一个函数。

就像您在 C++20 之前所做的那样。

如果您的 task 类型代表一个异步任务,那么在适当的 RAII 风格中,它的析构函数应该等到任务完成后再退出(这包括在整个所述任务的整个执行。任务在完全完成后才算完成)。因此,如果 handler() 在你的 schedule_coroutine 中调用 returns 一个 task,那么那个 task 将被初始化并立即销毁。由于析构函数等待异步任务完成,因此线程不会在任务完成之前死亡。并且由于线程的函子是 copied/moved 来自提供给 thread 构造函数的函数对象,任何捕获将继续存在,直到线程本身退出。

你的问题很有道理,误解是C++20 coroutines实际上是生成器错误地占用了coroutineheader名字。

让我解释一下生成器是如何工作的,然后回答如何安排分离协程。

发电机的工作原理

你的问题 Scheduling a detached coroutine 然后看起来 How to schedule a detached generator 答案是:not possible 因为 特殊约定 将常规函数转换为生成器函数。

不明显的是产生一个值必须发生在生成器函数body中。当您想 调用辅助函数 为您产生价值时 - 您不能。相反,您还将 辅助函数 制作成生成器,然后 await 而不是仅调用辅助函数。这有效地链接了生成器,并且可能感觉像是编写执行异步的同步代码。

在Javascript 特殊约定中async关键字。在 Python 特殊约定中 yield 而不是 return 关键字。

C++20 coroutines 是允许像 async/await 一样实现 Javascipt 的低级机制。

将此 low-level 机制包含在 C++ 语言中并没有错,只是将其放置在名为 coroutine.

的 header 中

如何安排分离协程

如果您想要 green threadsfibers 并且您正在编写使用对称或非对称协程来完成此任务的调度程序逻辑,那么这个问题就有意义了。

现在其他人可能会问:当您拥有发电机时,为什么还要为光纤(而不是 windows 光纤;)而烦恼? 答案是因为您可以封装 并发和并行逻辑,这意味着您团队的其他成员不需要学习和应用额外的心理体操,同时正在处理项目。

结果是 真正的异步编程 团队的其他成员编写线性代码,没有回调等,具有简单的并发概念,例如单个 spawn() 库函数,避免任何 locks/mutexes 和其他多线程复杂性。

当所有细节都隐藏在低级 i/o 方法中时,就会看到封装的美妙之处。所有上下文切换、调度等都发生在 i/o 类 的深处,如 ChannelQueueFile.

参与异步编程的每个人都应该体验过这样的工作。感觉很强烈。

要完成此操作而不是 C++20 coroutines,请使用允许对称协程的 Boost::fiber that includes scheduler or Boost::context。对称协程允许暂停和切换到任何其他协程,而不对称协程暂停和恢复调用协程。