俺的回收站

架构分析 解释编译原理
  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

概念

协程(Coroutine)这个概念最早是Melvin Conway在1963年提出的,是并发运算中的概念,指两个子过程通过相互协作完成某个任务,用它可以实现协作式多任务,协程(coroutine)技术本质上是一种程序控制机制。比如,消费者/生产者,你走几步,我走几步;下棋对弈,你一步我一步。
Coroutine(协程)可以分为:

  • 非对称式(asymmetric)协程,或称半对称式(semi-asymmetric)协程,又或干脆就叫半协程(semi-coroutine)
  • 对称式(symmetric)协程。

非对称式(asymmetric)协程之所以被称为非对称的,是因为它提供了两种传递程序控制权的操作:一种是(重)调用协程(通过coroutine.resume);另一种是挂起协程并将程序控制权返回给协程的调用者(通过coroutine.yield)。一个非对称协程可以看做是从属于它的调用者的,二者的关系非常类似于例程(routine)与其调用者之间的关系。对称式(symmetric)协程的特点是只有一种传递程序控制权的操作(coroutine.transfer),即将控制权直接传递给指定的协程。曾经有这么一种说法,对称式和非对称式协程机制的能力并不等价,但事实上很容易根据前者来实现后者。在不少动态脚本语言(Python、Perl,Lua,Ruby)都提供了协程或与之相似的机制。
对称式协程机制可以直接指定控制权传递的目标,拥有极大的自由,但得到这种自由的代价却是牺牲程序结构。如果程序稍微复杂一点,那么即使是非常有经验的程序员也很难对程序流程有全面而清晰的把握。这非常类似goto语句,它能让程序跳转到任何想去的地方,但人们却很难理解充斥着goto的程序。非对称式协程具有良好的层次化结构关系,(重)启动这些协程与调用一个函数非常类似:被(重)启动的协程得到控制权开始执行,然后挂起(或结束)并将控制权返回给协程调用者。这与结构化编程风格是完全一致的。

线程和协程的异同

协程(Coroutine)类似于线程(Thread)的地方是:每个协程都有有自己的堆栈,自己的局部变量。

线程和协程的主要区别在于:
 1. 线程可以并发运行,线程之间是不能共写全局变量(写冲突)。
 2. 协程不能并发运行,协程之间可以共享全局变量(不会存在写冲突)。

协程的实现和使用

创建协程

type TMeCoRoutineFunc =procedure(const aCoRoutine: TMeCoRoutine);
TMeCoRoutineMethod
=procedure() ofobject;
var func: TMeCoroutineFunc;
co
= TMeCoRoutine.create(func)

参数是一个函数,返回值是创建的协程对象。

协程的状态

协程有三种状态:挂起(suspended),运行(running),停止(dead)。
当我们创建一个协程时他开始的状态为挂起态,也就是说我们创建协程的时候不会自动运行。

st = co.status;

激活协程

IsSucessful := co.resume();

激活挂起的协程,使协程继续运行。参数co是一个协程对象。

如果协程是挂起状态,则继续运行,resume函数返回true。如果协程已经停止或者遇到其他错误,resume函数返回false。

挂起协程

co.yield([...]);

挂起当前协程。直到协程被外部协程使用CoRoutine.Resume再次激活,将返回到执行CoRoutine.Yield函数后的地方继续执行。
CoRoutine.yield的参数将传递给SaveYieldedValue虚方法,你需要重载该方法处理。

当一个协程正在运行时,不能在外部终止它.只能在协程内部调用coroutine.yield挂起当前协程。
不需要考虑协程安全、协程同步的问题。协程的代码比线程的代码更容易编写。

“控制”和“行为”的复用

在很多时候,我们需要对数据结构(如:List,Stack)中的元素按某种要求进行遍历,我们称之为“控制”;然后对目标元素进行某个操作(如,显示该元素),我们称之为“行为”。许多情况下,这种“控制”或行为的代码本来是可以被复用的,但是因为难以将这其中的“控制”和“行为”分离,造成了我们不得不一遍又一遍的书写这些类似的代码(虽然利用回调可以实现在一定程度上的“控制”和行为的分离,但是并不优雅,也不无法实现彻底重用)。

生产和消费

让我们先看下面一段代码,producer过程(生产者)产生一些数值(根据要求进行遍历),而Consumer过程(消费者)则处理值(对目标元素进行操作):

procedure producer();
var i: integer; begin for i :=0 to 100 do if i mod 5 = 0 then consumer(i);
end;
procedure Consumer(const value: integer);
begin writeln(value); end;

请注意在生产者(producer)调用消费过程(consumer)这里出现了耦合,该生产者只能为这个Cosumer过程服务。我们希望生产者过程(producer)能增强通用性,降低耦合度,能为不同的消费者服务。

ok,我们想到了回调:

type TComsumerCallback: procedure(const value:integer);
procedure producer(const aCallBack: TComsumerCallback);
var i: integer; begin for i :=0 to 100 do//循环枚举控制 if i mod 5=0then aCallBack(i);
end;

好了producer可以为不同的消费者服务了。但是,新的问题又出来。如果我们现在还希望仅当消费者要求值的时候才去调用生产者取得值呢?实现控制和行为的彻底的分离。象这样:

procedure MainConsumer();
begin //控制在MyProducer中,控制复用 for i in MyProducer do//当消费者要求值的时候才去调用 begin Consumer(i);
....
//随时可以停止从生产者取值 end;
end;

Visitor模式与Iterator模式

回调的实质是一种简单Visitor模式,说它简单是因为它只有Visitor,没有Visited部分。利用回调很难做到:消费者要,生产者才给,如果消费者不问不要,生产者就不答不给,这是由visitor模式的特性所决定的:回调的使用者把Callback函数扔到遍历算法里面,然后运行算法,同时祈祷并等候算法的完成(Push and Wait),使用者完全失去了控制权,只能等待算法整个完成或者中止,才能重新拿到控制权。而尽管使用Iterator模式能很容易的做到这一点(Iterator本质上属于问答模式,或者说消费者/生产者模式,Iterator的用法本身就是Lazy的,一问一答,遍历算法停在那里恭候Iterator使用者的调遣),但是如果放弃回调方式却又无法复用消费者了。要想同时做到既要复用“控制”,又要复用“行为”,这几乎是不可能的(当然如果是仅仅想实现“消费者要,生产者才给”那是可以的,不过难度比回调大,而且并不能通用各种数据结构),因为visitor模式和Iterator模式的特性恰恰是完全相反的:
 * Iterator是一种主动模型,Pull模型,Ask and Get。Iterator听候用户的调遣。
 * Vistor是一种被动模型,Push模型,Plugin / callback模型,Push and Pray and Wait。Visitor听候算法的调遣。

//利用Iterator模式实现按消费者需要取值:简单的数组、链表只要保存当前调用步骤(数组索引,
//或者当前指针)和调用环境(内部数据集)的结构,返回给用户就可以了。 //用户每次调用iterator.next,iterator就把索引或指针向后移动一下。 如果是内部数据复杂的Tree,
//Graph结构,就相当复杂了。比如是遍历一棵树,而且这棵树的Node里面没有Parent引用,那么Iterator
//必须自己维护一个栈把前面的所有的Parent Node都保存起来。
type TProducer =class private i: integer; public Current: Integer; function MoveNext: boolean;
end;
procedure TProducer.MoveNext;
var
 i: integer;
begin Result := False;
if i <= 100 then
begin

 
if i mod 5 = 0 then Current := i;
i :
= i +1;
Result :
= True;
end;
end;
procedure MainConsumer();
begin
with TProducer.Create do
try
while MoveNext do begin Consumer(Current); .... //随时可以停止从生产者取值 end;
finally Free; end;
end;

这时候,有聪明人就将目光转向了非对称式(asymmetric-coroutine)协程。不难看出这里面的Iterator就是Coroutine里面的生产者(数据提供者,所以有时我们也称之为Generator)。
一旦用户(消费者Consumer角色)调用了iterator.next(coroutine.Resume), Iterator就继续向下执行一步,然后把当前遇到的内部数据的Node放到一个消费者用户能够看到的公用的缓冲区(比如,直接放到消费者线程栈里面的局部变量)里面,然后自己就停下来(coroutine.Yield)。然后消费者用户就从缓冲区里面获得了那个Node。这样Iterator就可以自顾自地进行递归运算,不需要自己管理堆栈上下文,而是协程机制帮助它分配和管理运行栈。从而实现将“控制”和“行为”的彻底解藕。请看如下C#程序:

using System.Collections.Generic;
public class MyProducer : IEnumerable<string> { //iterator block,实现枚举元素控制 public IEnumerator<string> GetEnumerator()
{
for(int i =0; i<elements.Length; i++)
yield elements[i];
}
...
}
foreach (string item innew MyProducer())
{
//实现终端上打印元素的行为 Console.WriteLine(item);
}

在这段代码执行过程中,foreach 的循环体和 GetEnumerator 函数体实际上是在同一个线程中交替执行的。这是一种介于线程和顺序执行之间的协同执行模式,之所以称之为协同(Coroutine),是因为同时执行的多个代码块之间的调度是由逻辑隐式协同完成的。就协同执行而言,从功能上可以分为行为、控制两部分,控制又可进一步细分为控制逻辑和控制状态。行为对应着如何处理目标对象,如上述代码中:行为就是将目标对象打印到终端;控制则是如何遍历这个 elements 数组,可进一步细分为控制逻辑(顺序遍历)和控制状态(当前遍历到哪个元素)。其中心思想在于通过Coroutine程序控制机制和Yield将其行为与控制彻底分离,以此来进一步降低代码的耦合度,增强通用性,提高代码的复用率。