安排带有上下文的协同程序

Scheduling a coroutine with a context

本文关键字:程序 上下文      更新时间:2023-10-16

有很多教程解释了如何在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(最好是在协程承诺附近或之内),这样下次恢复协程时就不会试图访问被破坏的对象数据。但不幸的是,我不知道该怎么做

我认为您的问题是对co_await协同程序的工作方式的普遍(也是常见的)误解。

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

这样做的全部目的是使异步执行的代码看起来尽可能像同步代码。在同步代码中,您可以执行类似<expr>.wait()的操作,其中wait是一个等待expr表示的任务完成的函数。代替";等待";在上面,你"a-wait";或";异步等待";函数的其余部分相对于调用方异步执行,这取决于expr何时完成以及它决定如何恢复函数的执行。通过这种方式,co_await <expr>看起来和行为都非常像<expr>.wait()

编译器Magictm然后进入幕后,使其异步。

因此,推出";分离式协同程序";在这个框架内没有意义。协程函数的调用方(通常)不是决定协程在哪里执行的调用方;正是协同程序在执行过程中调用的进程决定了这一点。

你的CCD_ 13函数真的应该只是一个正则的";异步执行一个函数";活动它不应该与协程有任何特定的关联,也不应该期望给定的函子是或代表某个异步任务,或者它碰巧调用了co_await。这个函数只是创建一个新线程并在上面执行一个函数

正如您在C++20之前所做的那样。

如果你的task类型代表一个异步任务,那么在正确的RAII风格中,它的析构函数应该等到任务完成后再退出(这包括在所述任务的整个执行过程中由该任务调度的协程的任何恢复。直到完全完成,任务才完成)。因此,如果schedule_coroutine调用中的handler()返回task,则该task将被初始化并立即销毁。由于析构函数等待异步任务完成,线程在任务完成之前不会死亡。由于线程的函子是从给定给thread构造函数的函数对象中复制/移动的,因此任何捕获都将继续存在,直到线程本身退出。

我希望我说得对,但我认为这里可能有几个误解。首先,你显然无法分离协同程序,这根本没有任何意义。但是,您肯定可以在协同程序中执行异步任务,尽管在我看来这完全违背了它的目的。

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

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

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

您的问题很有道理,误解是C++20 coroutines实际上是错误地占用coroutine标头名称的生成器。

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

发电机的工作方式

您的问题Scheduling a detached coroutine然后查找How to schedule a detached generator,答案是:not possible,因为特殊约定将正则函数转换为生成函数。

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

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

C++20 coroutines是允许实现类似Javascipt的async/await的低级机制。

在C++语言中包含这个低级机制并没有错,只是将它放在名为coroutine的头中。

如何安排单独出游

如果您想要green threadsfibers,并且您正在编写使用对称或非对称协程来实现这一点的调度器逻辑,那么这个问题是有意义的。

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

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

当所有细节都隐藏在低级别的i/o方法中时,封装的美妙之处就显现出来了。所有上下文切换、调度等都发生在i/o类(如ChannelQueueFile)内部。

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

要实现这一点,请使用Boost::fiber,而不是C++20 coroutines,该fiber包含调度器或Boost::上下文,允许对称协程。对称协程允许挂起并切换到任何其他协程,而非对称协程则挂起并恢复调用协程。