安全的跨平台协程

9

我遇到的所有协程实现都使用汇编语言或检查 jmp_buf 的内容。这个问题在于它并不跨平台。

我认为以下实现不会出现未定义的行为,也不依赖于实现细节。但是我从未遇到过像这样编写的协程。

在使用线程时,使用 long jump 存在某些固有缺陷吗?
在此代码中是否存在某些隐藏的陷阱?

#include <setjmp.h>
#include <thread>

class Coroutine
{
public:
   Coroutine( void ) :
      m_done( false ),
      m_thread( [&](){ this->start(); } )
   { }

   ~Coroutine( void )
   {
      std::lock_guard<std::mutex> lock( m_mutex );

      m_done = true;
      m_condition.notify_one();

      m_thread.join();
   }

   void start( void )
   {
      if( setjmp( m_resume ) == 0 )
      {
         std::unique_lock<std::mutex> lock( m_mutex );
         m_condition.wait( lock, [&](){ return m_done; } );
      }
      else
      {
         routine();
         longjmp( m_yield, 1 );
      }
   }

   void resume( void )
   {
      if( setjmp( m_yield ) == 0 )
      {
         longjmp( m_resume, 1 );
      }
   }

   void yield( void )
   {
      if( setjmp( m_resume ) == 0 )
      {
         longjmp( m_yield, 1 );
      }
   }

private:
   virtual void routine( void ) = 0;

   jmp_buf m_resume;
   jmp_buf m_yield;

   bool m_done;
   std::mutex m_mutex;
   std::condition_variable m_condition;
   std::thread m_thread;
};

1
协程在上个世纪很流行。随着拥有多个核心的处理器变得普遍,它已经彻底过时了。除非出于学术兴趣,否则请利用线程并避免使用setjmp()带来的恐怖。 - Hans Passant
15
我对协程并发不感兴趣。它们有很多有用的功能,但穷人版本的并发不是其中之一。可以参考Lua示例,以及维基百科参考 - deft_code
6
@Hans Passant -- 协程绝对不会消失,无论处理器有多少个核心,因为上下文切换速度更快,您可以拥有比线程多两个数量级的协程,并且执行顺序有时很重要。 - Gene Bushuyev
2
@Hans Passant -- 我觉得我没有解释清楚。纤程(协程)不会产生内核上下文切换,它们在单个线程中运行,上下文切换就像长跳一样。我的第二个观点是,由于它们在单个线程中运行,它们是非抢占式的。不仅没有锁定、竞争等问题,纤程的执行顺序也是有保证的。它们是事件驱动模拟器中的基本原语,其中事件的顺序是至关重要的。它们不能被可抢占线程替代。 - Gene Bushuyev
3
我认为并发和并行的概念容易混淆。如果你研究一下“新兴”的编程语言,比如Go或Haskell,你会发现它们专门为并发而设计,并提供“轻量级”执行线程。它们并不本质上增加应用程序的并行性(因为最大并行度本来就受到硬件限制),但可以让你定义成千上万个轻量级任务,这些任务能够同时演变。在我看来,协程是为了并发而存在的,可能也适用于并行,但并不一定如此。 - Matthieu M.
显示剩余5条评论
4个回答

9

更新 2013-05-13 最近有一个Boost协程库(基于Boost上下文库构建,尚未在所有目标平台上实现,但很可能会在不久的将来支持所有主要平台)。


我不知道无栈协程是否适合您的预期用途,但我建议您在这里查看它们:

Boost Asio:Proactor设计模式:无需线程的并发处理

Asio还具有一种基于单个(如果我没记错)简单预处理器宏的协程“仿真”模型,结合一些巧妙设计的模板工具,可以接近编译器对无栈协程的支持。

示例HTTP服务器4就是这种技术的一个例子。

Boost Asio的作者Kohlhoff在他的博客中解释了机制和示例:无栈协程简易指南

一定要查看该系列的其他文章!


哇!感谢阅读。宏和行号与switch语句的结合确实很巧妙! - Matthieu M.

6

有一个C++标准提案,用于支持协程-N3708,由Oliver KowalkeBoost.Coroutine的作者)和Goodspeed撰写。

我认为这最终会成为终极清洁解决方案(如果真的发生了...)。因为我们没有来自C++编译器的栈交换支持,协程目前需要低级别(通常是汇编级别或setjmp/longjmp)的hack,这超出了C++的抽象范围。然后实现就很脆弱,需要编译器的帮助才能变得健壮。

例如,设置协程上下文的堆栈大小非常困难,如果溢出堆栈,则程序将默默地损坏。或者如果你幸运的话,会崩溃。分段堆栈似乎可以帮助解决这个问题,但是这又需要编译器级别的支持。

一旦成为标准,编译器编写者将会照顾到它。但在那一天之前,对我来说,Boost.Coroutine将是C++中唯一实用的解决方案。

在C语言中,有由Go团队成员Russ Cox编写的libtasklibtask工作得相当不错,但似乎已经不再维护。
附注:如果有人知道如何支持标准提案,请告诉我。我真的支持这个提案。

我在C++和C中都使用protothreads。事实上,上述协程只是重新打包的protothreads宏。 - Martin

4

没有通用的跨平台实现协程的方式。尽管一些实现可以使用setjmp/longjmp来强制实现协程,但这种做法不符合标准。如果routine1使用setjmp()创建jmp_buf1,然后调用routine2()使用setjmp()创建jmp_buf2,任何对jmp_buf1的longjmp()都会使jmp_buf2无效(如果它还没有被使无效的话)。

我在各种CPU上都实现过协程,但我总是使用至少一些汇编代码。通常不需要太多(例如,在8x51上进行任务切换只需要四条指令),但使用汇编代码可以确保编译器不会应用破坏一切的创意优化。


你不需要使用longjmp。实际上,这是不好的并且会消耗内存。你可以通过仅使用宏将协程实现为隐式状态机。这既提供了顺序查看代码,又几乎没有内存开销。 - Martin
1
@Martin:我已经完成了基于状态机的伪多任务处理,但我不称固定入口点状态机为“协程”。对于我来说,要考虑一个系统是否支持“真正”的协程,必须能够在子例程调用中切换执行上下文,从而使得阻塞方法成为可能。 - supercat
@supercat:您能详细说明一下“任何longjmp()到jmp_buf1都会使jmp_buf2无效”的确切机制吗?我认为您只是在暗示跳回routine1将弹出routine2的帧,以至于之后再跳回routine2就没有意义了;但是我已经在这个问题上把自己搞糊涂了。 :) 如果我正确理解了上述情况,那么通过我描述的机制跳转到jmp_buf1将使jmp_buf2无效;但是跳转到jmp_buf2将不会使jmp_buf1无效。对吗? - Quuxplusone
@Quuxplusone:这个“机制”的意思是,标准规定如果代码尝试在jmp_buf的创建上下文之外使用它,则不会对其造成任何要求。无论是否会发生任何不良后果都取决于许多因素,其中一些可能是可预测的,而另一些则不是。 - supercat

2

我不相信你能通过长跳来完全实现协程。在WinAPI中,协程是本地支持的,称为Fiber。例如,请参见CreateFiber()。我认为其他操作系统没有本地协程支持。如果您查看SystemC库,其中协程是中心部分,则对于每个受支持的平台,它们都是使用汇编实现的,除了Windows。 GBL库也使用协程进行基于Windows Fiber的事件驱动模拟。尝试实现协程和事件驱动设计时很容易产生难以调试的错误,因此建议使用已经经过彻底测试并具有更高级抽象来处理这个概念的现有库。


协程并不是操作系统的领域,而是应用程序特定的功能,存在于用户空间中。 - Martin

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