浅谈C#中的代码协同(Coroutine)执行支持

发表于:2007-06-17来源:作者:点击数: 标签:
几个月前我曾大致分析过 C# 2.0 中 iterator block 机制的实现原理,《C# 2.0 中Iterators的改进与实现原理浅析》,文中简要介绍了 C# 2.0 是如何在不修改 CLR 的前提下由编译器,通过有限状态机来实现 iterator block 中 yield 关键字。 实际上,这一机制的

   
  几个月前我曾大致分析过 C# 2.0 中 iterator block 机制的实现原理,《C# 2.0 中Iterators的改进与实现原理浅析》,文中简要介绍了 C# 2.0 是如何在不修改 CLR 的前提下由编译器,通过有限状态机来实现 iterator block 中 yield 关键字。

  实际上,这一机制的最终目的是提供一个代码协同执行的支持机制。
  以下内容为程序代码:
  
  using System.Collections.Generic;
  
  public class Tokens : IEnumerable
  {
  public IEnumerator GetEnumerator()
  {
  for(int i = 0; i  yield elements[i];
  }
  ...
  }
  
  foreach (string item in new Tokens())
  {
  Console.WriteLine(item);
  }
  
  在这段代码执行过程中,foreach 的循环体和 GetEnumerator 函数体实际上是在同一个线程中交替执行的。这是一种介于线程和顺序执行之间的协同执行模式,之所以称之为协同(Coroutine),是因为同时执行的多个代码块之间的调度是由逻辑隐式协同完成的。顺序执行无所谓并行性,而线程往往是由系统调度程序强制性抢先切换,相对来说Win3.x 中的独占式多任务倒是与协同模型比较类似。
  就协同执行而言,从功能上可以分为行为、控制两部分,控制又可进一步细分为控制逻辑和控制状态。行为对应着如何处理目标对象,如上述代码中:行为就是将目标对象打印到控制台;控制则是如何遍历这个 elements 数组,可进一步细分为控制逻辑(顺序遍历)和控制状态(当前遍历到哪个元素)。下面将按照这个逻辑介绍不同语言中如何实现和模拟这些逻辑。
  
  Spark Gray 在其 blog 上有一个系列文章介绍了协同执行的一些概念。
  
  Iterators in Ruby (Part - 1)
  Warming up to using Iterators (Part 2)
  
  文章第 1, 2 部分以 Ruby 语言(语法类似 Python)介绍了 Iterator 机制是如何简化遍历操作的代码。实际上中心思想就是将行为与控制分离,由语言层面的支持来降低控制代码的薄记工作。
  以下内容为程序代码:
  
  def textfiles(dir)
  Dir.chdir(dir)
  
  Dir["*"].each do |entry|
  yield dir+"\"+entry if /^.*.txt$/ =~ entry
  
  if FileTest.directory?(entry)
  textfiles(entry){|file| yield dir+"\"+file}
  end
  end
  Dir.chdir(".."[img]/images/wink.gif[/img]
  end
  
  textfiles(“c:\”){|file|
  puts file
  }
  
  例如上面这段 Ruby 的递归目录处理代码中,就采用了与 C# 2.0 中完全类似的语法实现协同执行支持。
  
  对 C# 1.0 和 C++ 这类不支持协同执行的语言,协同执行过程中的状态迁移或者说执行绪的调度工作,需要由库和使用者自行实现,例如 STL 中的迭代器 (iterator) 自身必须保存了与遍历容器相关的位置信息。例如在 STL 中实现协同执行:
  以下内容为程序代码:
  
  #include
  #include
  #include
  
  // The function object multiplies an element by a Factor
  template
  class MultValue
  {
  private:
  Type Factor;  // The value to multiply by
  public:
  // Constructor initializes the value to multiply by
  MultValue ( const Type& _Val [img]/images/wink.gif[/img] : Factor ( _Val [img]/images/wink.gif[/img] {
  }
  
  // The function call for the element to be multiplied
  void operator ( [img]/images/wink.gif[/img] ( Type& elem [img]/images/wink.gif[/img] const
  {
  elem *= Factor;
  }
  };
  
  int main( [img]/images/wink.gif[/img]
  {
  using namespace std;
  
  vector v1;
  
  //...
  
  // Using for_each to multiply each element by a Factor
  for_each ( v1.begin ( [img]/images/wink.gif[/img] , v1.end ( [img]/images/wink.gif[/img] , MultValue ( -2 [img]/images/wink.gif[/img] [img]/images/wink.gif[/img];
  }
  
  虽然 STL 较为成功的通过迭代器、算法和谓词,将此协同执行逻辑中的行为和控制分离,谓词表现行为(MultValue、迭代器(v1.being(), v1.end())表现控制状态、算法表现控制逻辑(for_each),但仍然存在编写复杂,使用麻烦,并且语义不连冠的问题。
  一个缓解的方法是将谓词的定义与控制部分合并到一起,就是类似 boost::Lambda 的实现思路:
  以下内容为程序代码:
  
  for_each(v.begin(), v.end(), _1 = 1);
  
  for_each(vp.begin(), vp.end(), cout << *_1 << ' ');
  
  通过神奇的模板和宏,可以一定程度降低编写独立谓词来定义行为的复杂度。但控制部分的状态和逻辑还是需要单独实现。
  
  而 C# 1.0 中就干脆没有自带支持,必须通过《C# 2.0 中Iterators的改进与实现原理浅析》一文中所举例子那样笨拙的方式完成。
  以下内容为程序代码:
  
  public class Tokens : IEnumerable
  {
  public string[] elements;
  
  Tokens(string source, char[] delimiters)
  {
  // Parse the string into tokens:
  elements = source.Split(delimiters);
  }
  
  public IEnumerator GetEnumerator()
  {
  return new TokenEnumerator(this);
  }
  
  // Inner class implements IEnumerator interface:
  private class TokenEnumerator : IEnumerator
  {
  private int position = -1;
  private Tokens t;
  
  public TokenEnumerator(Tokens t)
  {
  this.t = t;
  }
  
  // Declare the MoveNext method required by IEnumerator:
  public bool MoveNext()
  {
  if (position < t.elements.Length - 1)
  {
  position++;
  return true;
  }
  else
  {
  return false;
  }
  }
  
  // Declare the Reset method required by IEnumerator:
  public void Reset()
  {
  position = -1;
  }
  
  // Declare the Current property required by IEnumerator:
  public object Current
  {
  get // get_Current函数
  {
  return t.elements[position];
  }
  }
  }
  ...
  }
  
  这种笨拙的 IEnumerable 接口实现方法,实际上是将 STL 中提供控制状态的 iterator 完全自行实现,而且控制逻辑还限定于编写 IEnumerable 接口实现时的定义。就算可以通过策略 (Strategy) 模式提供一定程度的定制,但其代码逻辑过于分散,要理解一个简单调用必须查看四五处分散的代码。
  
  好在牛人总是不缺的,呵呵。
  
  Ajai Shankar 在 MSDN 上一篇非常出色的文章,COROUTINES Implementing Coroutines for .NET by Wrapping the Unmanaged Fiber API,里面通过 Win32 API 的纤程 (Fiber) 支持和 CLR 几个底层 API 的支持,完整的实现了一套可用的协同执行支持机制。
  Spark Gray 的第 4 篇文章中就详细讨论了这种实现方式的利弊:
  
  SICP, Fiber api and ITERATORS !(Part 4)
  
  纤程 Fiber 是 Win32 子系统为了移植 Unix 下伪线程环境下的程序方便,而提供的一套轻量级并行执行机制,由程序代码自行控制调度流程。
  其使用方法很简单,在某个线程中调用 ConvertThreadToFiber(Ex) 初始化纤程支持,然后调用 CreateFiber(Ex) 建立多个不同纤程,对新建的纤程和转换时当前线程缺省纤程,都可以通过 SwitchToFiber 显式进行调度。
  以下内容为程序代码:
  
  static int array[3] = { 0, 1, 2 };
  
  static int cur = 0;
  
  VOID CALLBACK FiberProc(PVOID lpParameter)
  {
  for(int i=0; i  {
  cur = array[i];
  
  SwitchToFiber(lpParameter);
  }
  }
  
  LPVOID fiberMain = ConvertThreadToFiber(NULL);
  
  LPVOID fiberFor = CreateFiber(0, FiberProc, fiberMain);
  
  while(cur >= 0)
  {
  std::cout << cur << std::endl;
  
  SwitchToFiber(fiberFor);
  }
  
  DeleteFiber(fiberFor);
  
  上述伪代码是纤程使用的一个大概流程,可以看出实际上纤程跟上面 Ruby 和 C# 2.0 中的协同执行所需功能是非常符合的。而在实现上,纤程实际上是通过在同一线程堆栈中构造出不同的区域(ConvertThreadToFiber/CreateFiber),在 SwitchToFiber 函数中切换到指定区域,以此区域(纤程)的代码和寄存器等环境执行,有点类似于 C 代码库中 longjmp 的概念。Netscape 提供的状态线程库 State Threads library 就是通过 longjmp 等机制模拟的类似功能。
  而在 .NET 1.0/1.1 中要使用纤程,则还需要考虑对每个纤程的 Managed 环境构造,以及切换调度时的状态管理等等。有兴趣的朋友可以仔细阅读上述两篇精彩文章。
  以下内容为程序代码:
  
  class CorIter : Fiber {
  protected o

原文转自:http://www.ltesting.net