问题 C ++ 1z协同线程上下文和协同调度


根据这个最新的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的调度程序,它可以在线程上执行协同程序 协程开始了。代码可以在这个github repo中找到: https://github.com/llint/Awaitable 

Awaitable是一种可组合的方式,它在嵌套级别维护正确的调用排序,并且它具有原始的让步,定时等待和从其他地方设置就绪,并且可以从中导出非常复杂的使用模式(例如只有无限循环协程当某些事件发生时被唤醒),编程模型紧跟C#Task async / await模式。请随时提供反馈。


1048
2018-01-01 05:05


起源

好问题!至于C#中的任务调度,它全部打开 github上,给出一些很好的见解。至于c ++,其中一个建议 n4286 (在起草之前)涵盖了针对boost的未来的演示实现,但似乎'谁'调用延续真的会依赖于impl - Oleg Bogdanov
我认为你的问题同样适用于线程/上下文 未来::然后 将被称为,即undefined¯_(ツ)_ /¯ - Oleg Bogdanov
感谢您的评论。然而,对于“谁”调用延续,我推测应该有一些提议的标准措辞或设施将支持自定义单线程协同调度程序的实现 - 例如如果所有内容都将在主线程上执行,那么应该有一个主调度程序循环(或勾​​号),可以在主线程上调用所有已调度的“任务一半”将被执行 - 或“主”线程可以是我针对更无法控制的线程池线程选择的任何线程。 - Dejavu
提案 说,执行者的概念被涵盖在内 n3731 - Oleg Bogdanov
它就在那里,甚至有 同名。更难找回来,concurt没有记录得很好。您可能没有在同一个线程上看到延续恢复,因为您使用默认调度程序。类库的工作是支持应用程序模型来实现这一点,线程必须合作并解决生产者 - 消费者问题(也称为调度程序循环)。当你定位WinRT(又名UWP)项目时,你得到一个。 - Hans Passant


答案:


相反!

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

埃维 co_await 期待等待类型。简而言之,等待类型是提供以下三种功能的类型:

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

所以当你打电话 co_await 在等待类型,程序要求等待是否应该暂停协程(如果 await_ready 返回false)如果是这样的话 - 你得到一个协程句柄,你可以随意做任何事情。

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

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

你可以将coroutine句柄附加到派生类中 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();
    } 

那么为什么你会看到一个win32线程池线程,如果协程是以常规方式启动的 std::thread?那是因为它不是协程。 std::async 在幕后打电话给 concurrency::create_task。一个 concurrency::task 默认情况下在win32线程池下启动。毕竟,整个目的 std::async 是在另一个线程中启动callable。


10
2018-01-01 07:57



优秀!这个答案似乎澄清了协同程序的线程/执行上下文之谜。似乎为了支持future.then(又称任务延续),似乎仍然需要一个任务调度器,只能安排继续运行(可能在同一个线程上下文,具体取决于具体的调度程序)实现)当前任务/未来完成时 - VC ++方法使用任务调度程序,因为它依赖于不同线程上的阻塞等待,然后在该线程上恢复协程。 - Dejavu
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 获取最终值只是一个钩子,在某些情况下可能会留空 - Oleg Bogdanov