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

18
根据最新的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#中,任务调度的等待条件纯粹是通过任务继续完成实现的——通过一个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可以按嵌套级别维护正确的调用顺序,它具有原始的yielding、定时等待和从其他地方设置ready的特性,非常复杂的使用模式可以从中派生出来(例如只有在发生某些事件时才会被唤醒的无限循环协程),编程模型紧密遵循C#任务异步/等待模式。请随意提供您的反馈。

1
很好的问题!关于C#中的任务调度,所有内容都在github上公开,提供了一些很好的见解。至于C ++,其中一个提案n4286(在成为草案之前)涵盖了对boost未来的演示实现,但似乎“谁”调用继续真正取决于实现。 - Oleg Bogdanov
1
感谢评论。然而,关于“谁”调用续延,我会推测应该有一些拟议的标准措辞或设施,来支持自定义单线程协程调度器的实现——例如,如果所有东西都要在主线程上执行,就应该有一个主调度循环(或tick)可以在主线程上调用,所有预定的“任务半部分”将在其上执行——或者“主”线程可以是我选择的任何线程,而不是更不可控的线程池线程。 - Dejavu
1
提案 表示,执行器的概念在 n3731 中得到了涵盖。 - Oleg Bogdanov
1
它确实存在,甚至有相同的名称。只是比较难找到,因为concurt没有很好的文档记录。你可能没有在同一线程上看到续传恢复,因为你使用了默认调度程序。支持应用程序模型的类库的工作是把这个问题解决好,线程必须合作并解决生产者-消费者问题(也称为调度器循环)。当你针对WinRT(又称UWP)项目时,你会得到一个。 - Hans Passant
关于提到winRT注释,你可以通过从他们的源代码中借鉴,复制非常相似的行为。 - Mgetz
显示剩余3条评论
1个回答

18

相反的情况!

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 - 您自己创建的线程将恢复协程。

您可以将协程句柄附加到 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_task。默认情况下,concurrency::task 在win32线程池下启动。毕竟,std::async的整个目的就是在另一个线程中启动可调用对象。


太好了!这个答案似乎澄清了协程的线程/执行上下文的神秘之处。不过,为了支持 future.then(又名任务继续),仍然需要一个任务调度器,只有当当前任务/future完成时,继续才会被安排运行(可能在相同的线程上下文中,具体取决于特定的调度程序实现)- VC++方法避免使用任务调度程序,因为它依赖于在不同线程上的阻塞等待,然后在该线程上恢复协程。 - Dejavu
告诉线程在恢复协程时该做什么以及从co_await返回什么。但我不确定它是否完全准确,因为恢复将在“暂停-恢复点”中发生,而coroutine_handle的operator()()则告诉我们“该做什么”。 await_resume只是一个钩子,用于获取最终值,在某些情况下可能为空。 - Oleg Bogdanov

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接