【问题标题】:C++1z coroutine threading context and coroutine schedulingC++1z协程线程上下文和协程调度
【发布时间】:2023-04-15 17:34:01
【问题描述】:

根据这个最新的 C++ TS:http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/n4628.pdf,基于对 C# async/await 语言支持的理解,我想知道 C++ 协程的“执行上下文”(从 C# 借用的术语)是什么?

我在 Visual C++ 2017 RC 中的简单测试代码显示,协程似乎总是在线程池线程上执行,应用程序开发人员几乎无法控制协程可以在哪个线程上下文上执行 - 例如应用程序是否可以强制所有协程(使用编译器生成的状态机代码)仅在主线程上执行, 不涉及任何线程池线程?

在 C# 中,SynchronizationContext 是一种指定“上下文”的方法,其中所有协程“半”(编译器生成的状态机代码)将被发布和执行,如本文所示:https://blogs.msdn.microsoft.com/pfxteam/2012/01/20/await-synchronizationcontext-and-console-apps/,而当前协程Visual C++ 2017 RC 中的实现似乎总是依赖于并发运行时,它默认在线程池线程上执行生成的状态机代码。用户应用程序是否可以使用类似的同步上下文概念将协程执行绑定到特定线程?

此外,在 Visual C++ 2017 RC 中实现的协程的当前默认“调度程序”行为是什么?即 1) 如何准确指定等待条件? 2)当等待条件满足时,谁调用挂起的协程的“下半部分”?

我对 C# 中的任务调度的(天真的)猜测是,C# 纯粹通过任务继续来“实现”等待条件 - 等待条件由 TaskCompletionSource 拥有的任务合成,任何需要等待的代码逻辑都将被链接为它的延续,所以如果满足等待条件,例如如果从低级网络处理程序接收到完整消息,它会执行 TaskCompletionSource.SetValue,它将底层任务转换为完成状态,有效地允许链接的继续逻辑开始执行(将任务从以前创建的状态) - 在 C++ 协程中,我推测 std::future 和 std::promise 将用作类似的机制(std::future 是任务,而 std::promise 是 TaskCompletionSource,以及用法也惊人地相似!)- C++ 协程调度程序(如果有的话)是否依赖某种类似的机制来执行行为?

[编辑]:经过进一步研究,我能够编写一个非常简单但非常强大的抽象,称为 awaitable,它支持单线程和协作多任务处理,并具有一个简单的基于 thread_local 的调度程序,可以在线程上执行协程root 协程启动。代码可以从这个 github repo 中找到:https://github.com/llint/Awaitable

Awaitable 是可组合的,它在嵌套级别保持正确的调用顺序,并且它具有原始屈服、定时等待和从其他地方设置就绪的特点,并且可以从中派生出非常复杂的使用模式(例如无限循环协程仅在某些事件发生时才被唤醒),编程模型紧密遵循基于 C# 任务的异步/等待模式。请随时提供您的反馈。

【问题讨论】:

  • 好问题!至于 C# 中的任务调度,它全部在 github 开放,提供了一些很好的见解。至于 c++,其中一个提案n4286(在起草之前)涵盖了 boost 未来的演示实现,但它似乎“谁”调用 c​​ontinuation 真的会依赖于 impl
  • 我认为您的问题同样适用于将调用future::then 的线程/上下文,即未定义¯_(ツ)_/¯
  • 感谢 cmets。然而,至于“谁”调用延续,我推测应该有一些建议的标准措辞或工具来支持自定义单线程协程调度程序的实现 - 例如。如果一切都将在主线程上执行,则应该有一个可以在主线程上调用的主调度程序循环(或滴答声),所有计划的“任务半”都将在主线程上执行 - 或“主”线程可以是我针对更不可控的线程池线程选择的任何线程。
  • Proposal 表示 Executor 的概念包含在 n3731
  • 它就在那里,甚至还有the same name。只是更难找到,concurt 没有很好的记录。您可能没有在同一线程上看到继续恢复,因为您使用的是默认调度程序。支持应用程序模型的类库的工作是正确的,线程必须合作并解决生产者-消费者问题(也就是有一个调度程序循环)。当您以 WinRT(又名 UWP)项目为目标时,您会得到一个。

标签: c++ multithreading async-await coroutine c++-coroutine


【解决方案1】:

相反!

C++ 协程是关于控制的。这里的关键是
void await_suspend(std::experimental::coroutine_handle<> handle) 功能。

evey co_await 期望等待类型。简而言之,awaitable 类型是提供这三个功能的类型:

  1. bool await_ready() - 程序应该停止协程的执行吗?
  2. void await_suspend(handle) - 程序向您传递该协程框架的延续上下文。如果您激活句柄(例如,通过调用句柄提供的operator () - 当前线程立即恢复协程)。
  3. T await_resume() - 告诉恢复协程的线程在恢复协程时要做什么以及从co_await返回什么。

所以当你在 awaitable 类型上调用 co_await 时,程序会询问 awaitable 协程是否应该暂停(如果 await_ready 返回 false),如果是 - 你会得到一个协程句柄,你可以在其中做任何你喜欢的事情.

例如,您可以将协程句柄传递给线程池。在这种情况下,线程池线程将恢复协程。

您可以将协程句柄传递给一个简单的std::thread - 您的自己的创建线程将恢复协程。

您可以将协程句柄附加到 OVERLAPPED 的派生类中,并在异步 IO 完成时恢复协程。

如您所见 - 您可以通过管理 await_suspend 中传递的协程句柄来控制协程暂停和恢复的位置和时间。没有“默认调度程序”——如何实现等待类型将决定协程如何调度。

那么,在 VC++ 中会发生什么?不幸的是,std::future 仍然没有then 函数,所以你不能将协程句柄传递给std::future。如果您在 std::future 上等待 - 该程序将只打开一个新线程。看future标头给出的源码:

template<class _Ty>
    void await_suspend(future<_Ty>& _Fut,
        experimental::coroutine_handle<> _ResumeCb)
    {   // change to .then when future gets .then
    thread _WaitingThread([&_Fut, _ResumeCb]{
        _Fut.wait();
        _ResumeCb();
    });
    _WaitingThread.detach();
    } 

那么,如果协程在常规 std::thread 中启动,为什么您会看到一个 win32 线程池线程?那是因为它不是协程。 std::async 在幕后致电concurrency::create_taskconcurrency::task 默认在win32线程池下启动。毕竟,std::async 的全部目的是在另一个线程中启动可调用对象。

【讨论】:

  • 太棒了!这个答案似乎澄清了协程的线程/执行上下文的奥秘。似乎为了支持future.then(又名任务延续),似乎仍然需要一个任务调度程序,该延续只会被安排运行(可能在同一个线程上下文中,具体取决于特定的调度程序实现)当当前任务/未来完成时 - VC++ 方法使用任务调度程序绕过,因为它依赖于不同线程上的阻塞等待,然后在该线程上恢复协程。
  • tells the thread which resumes the coroutine what to do when resuming the coroutine and what to return from co_await. 我不确定它是否完全准确,但恢复会发生在“suspend-resume-point”及其告诉“做什么”的 coroutine_handle 的 operator()() 中。 await_resume 只是一个获取最终值的钩子,在某些情况下可能会留空
最近更新 更多