協(xié)程(Coroutine)這個(gè)概念最早是MelvinConway在1963年提出的,是并發(fā)運(yùn)算中的概念,指兩個(gè)子過(guò)程通過(guò)相互協(xié)作完成某個(gè)任務(wù),用它可以實(shí)現(xiàn)協(xié)作式多任務(wù),協(xié)程(coroutine)技術(shù)本質(zhì)上是一種程序控制機(jī)制。比如,消費(fèi)者/生產(chǎn)者,你走幾步,我走幾步;下棋對(duì)弈,你一步我一步。
Coroutine(協(xié)程)可以分為:
非對(duì)稱式(asymmetric)協(xié)程之所以被稱為非對(duì)稱的,是因?yàn)樗峁┝藘煞N傳遞程序控制權(quán)的操作:一種是(重)調(diào)用協(xié)程(通過(guò)coroutine.resume);另一種是掛起協(xié)程并將程序控制權(quán)返回給協(xié)程的調(diào)用者(通過(guò)coroutine.yield)。一個(gè)非對(duì)稱協(xié)程可以看做是從屬于它的調(diào)用者的,二者的關(guān)系非常類似于例程(routine)與其調(diào)用者之間的關(guān)系。對(duì)稱式(symmetric)協(xié)程的特點(diǎn)是只有一種傳遞程序控制權(quán)的操作(coroutine.transfer),即將控制權(quán)直接傳遞給指定的協(xié)程。曾經(jīng)有這么一種說(shuō)法,對(duì)稱式和非對(duì)稱式協(xié)程機(jī)制的能力并不等價(jià),但事實(shí)上很容易根據(jù)前者來(lái)實(shí)現(xiàn)后者。在不少動(dòng)態(tài)腳本語(yǔ)言(Python、Perl,Lua,Ruby)都提供了協(xié)程或與之相似的機(jī)制。
對(duì)稱式協(xié)程機(jī)制可以直接指定控制權(quán)傳遞的目標(biāo),擁有極大的自由,但得到這種自由的代價(jià)卻是犧牲程序結(jié)構(gòu)。如果程序稍微復(fù)雜一點(diǎn),那么即使是非常有經(jīng)驗(yàn)的程序員也很難對(duì)程序流程有全面而清晰的把握。這非常類似goto語(yǔ)句,它能讓程序跳轉(zhuǎn)到任何想去的地方,但人們卻很難理解充斥著goto的程序。非對(duì)稱式協(xié)程具有良好的層次化結(jié)構(gòu)關(guān)系,(重)啟動(dòng)這些協(xié)程與調(diào)用一個(gè)函數(shù)非常類似:被(重)啟動(dòng)的協(xié)程得到控制權(quán)開始執(zhí)行,然后掛起(或結(jié)束)并將控制權(quán)返回給協(xié)程調(diào)用者。這與結(jié)構(gòu)化編程風(fēng)格是完全一致的。
協(xié)程(Coroutine)類似于線程(Thread)的地方是:每個(gè)協(xié)程都有有自己的堆棧,自己的局部變量。
線程和協(xié)程的主要區(qū)別在于:
1. 線程可以并發(fā)運(yùn)行,線程之間是不能共寫全局變量(寫沖突)。
2. 協(xié)程不能并發(fā)運(yùn)行,協(xié)程之間可以共享全局變量(不會(huì)存在寫沖突)。
typeTMeCoRoutineFunc =procedure(const aCoRoutine: TMeCoRoutine);
TMeCoRoutineMethod =procedure() ofobject;
varfunc: TMeCoroutineFunc;
co = TMeCoRoutine.create(func)
參數(shù)是一個(gè)函數(shù),返回值是創(chuàng)建的協(xié)程對(duì)象。
協(xié)程有三種狀態(tài):掛起(suspended),運(yùn)行(running),停止(dead)。
當(dāng)我們創(chuàng)建一個(gè)協(xié)程時(shí)他開始的狀態(tài)為掛起態(tài),也就是說(shuō)我們創(chuàng)建協(xié)程的時(shí)候不會(huì)自動(dòng)運(yùn)行。
st = co.status;
激活協(xié)程
IsSucessful := co.resume();
激活掛起的協(xié)程,使協(xié)程繼續(xù)運(yùn)行。參數(shù)co是一個(gè)協(xié)程對(duì)象。
如果協(xié)程是掛起狀態(tài),則繼續(xù)運(yùn)行,resume函數(shù)返回true。如果協(xié)程已經(jīng)停止或者遇到其他錯(cuò)誤,resume函數(shù)返回false。
掛起協(xié)程
co.yield([...]);
掛起當(dāng)前協(xié)程。直到協(xié)程被外部協(xié)程使用CoRoutine.Resume再次激活,將返回到執(zhí)行CoRoutine.Yield函數(shù)后的地方繼續(xù)執(zhí)行。
CoRoutine.yield的參數(shù)將傳遞給SaveYieldedValue虛方法,你需要重載該方法處理。
當(dāng)一個(gè)協(xié)程正在運(yùn)行時(shí),不能在外部終止它.只能在協(xié)程內(nèi)部調(diào)用coroutine.yield掛起當(dāng)前協(xié)程。
不需要考慮協(xié)程安全、協(xié)程同步的問(wèn)題。協(xié)程的代碼比線程的代碼更容易編寫。
在很多時(shí)候,我們需要對(duì)數(shù)據(jù)結(jié)構(gòu)(如:List,Stack)中的元素按某種要求進(jìn)行遍歷,我們稱之為“控制”;然后對(duì)目標(biāo)元素進(jìn)行某個(gè)操作(如,顯示該元素),我們稱之為“行為”。許多情況下,這種“控制”或行為的代碼本來(lái)是可以被復(fù)用的,但是因?yàn)殡y以將這其中的“控制”和“行為”分離,造成了我們不得不一遍又一遍的書寫這些類似的代碼(雖然利用回調(diào)可以實(shí)現(xiàn)在一定程度上的“控制”和行為的分離,但是并不優(yōu)雅,也不無(wú)法實(shí)現(xiàn)徹底重用)。
讓我們先看下面一段代碼,producer過(guò)程(生產(chǎn)者)產(chǎn)生一些數(shù)值(根據(jù)要求進(jìn)行遍歷),而Consumer過(guò)程(消費(fèi)者)則處理值(對(duì)目標(biāo)元素進(jìn)行操作):
procedure producer();
vari: integer;begin for i :=0 to 100 do if i mod 5 = 0 then consumer(i);
end;
procedure Consumer(const value: integer);
beginwriteln(value);end;
請(qǐng)注意在生產(chǎn)者(producer)調(diào)用消費(fèi)過(guò)程(consumer)這里出現(xiàn)了耦合,該生產(chǎn)者只能為這個(gè)Cosumer過(guò)程服務(wù)。我們希望生產(chǎn)者過(guò)程(producer)能增強(qiáng)通用性,降低耦合度,能為不同的消費(fèi)者服務(wù)。
ok,我們想到了回調(diào):
typeTComsumerCallback: procedure(const value:integer);
procedure producer(const aCallBack: TComsumerCallback);
vari: integer;begin for i :=0 to 100 do//循環(huán)枚舉控制 if i mod 5=0then aCallBack(i);
end;
好了producer可以為不同的消費(fèi)者服務(wù)了。但是,新的問(wèn)題又出來(lái)。如果我們現(xiàn)在還希望僅當(dāng)消費(fèi)者要求值的時(shí)候才去調(diào)用生產(chǎn)者取得值呢?實(shí)現(xiàn)控制和行為的徹底的分離。象這樣:
procedure MainConsumer();
begin //控制在MyProducer中,控制復(fù)用 for i in MyProducer do//當(dāng)消費(fèi)者要求值的時(shí)候才去調(diào)用 beginConsumer(i);
.... //隨時(shí)可以停止從生產(chǎn)者取值 end;
end;
回調(diào)的實(shí)質(zhì)是一種簡(jiǎn)單Visitor模式,說(shuō)它簡(jiǎn)單是因?yàn)樗挥蠽isitor,沒(méi)有Visited部分。利用回調(diào)很難做到:消費(fèi)者要,生產(chǎn)者才給,如果消費(fèi)者不問(wèn)不要,生產(chǎn)者就不答不給,這是由visitor模式的特性所決定的:回調(diào)的使用者把Callback函數(shù)扔到遍歷算法里面,然后運(yùn)行算法,同時(shí)祈禱并等候算法的完成(PushandWait),使用者完全失去了控制權(quán),只能等待算法整個(gè)完成或者中止,才能重新拿到控制權(quán)。而盡管使用Iterator模式能很容易的做到這一點(diǎn)(Iterator本質(zhì)上屬于問(wèn)答模式,或者說(shuō)消費(fèi)者/生產(chǎn)者模式,Iterator的用法本身就是Lazy的,一問(wèn)一答,遍歷算法停在那里恭候Iterator使用者的調(diào)遣),但是如果放棄回調(diào)方式卻又無(wú)法復(fù)用消費(fèi)者了。要想同時(shí)做到既要復(fù)用“控制”,又要復(fù)用“行為”,這幾乎是不可能的(當(dāng)然如果是僅僅想實(shí)現(xiàn)“消費(fèi)者要,生產(chǎn)者才給”那是可以的,不過(guò)難度比回調(diào)大,而且并不能通用各種數(shù)據(jù)結(jié)構(gòu)),因?yàn)関isitor模式和Iterator模式的特性恰恰是完全相反的:
*Iterator是一種主動(dòng)模型,Pull模型,Ask and Get。Iterator聽候用戶的調(diào)遣。
*Vistor是一種被動(dòng)模型,Push模型,Plugin / callback模型,Push and Pray and Wait。Visitor聽候算法的調(diào)遣。
//利用Iterator模式實(shí)現(xiàn)按消費(fèi)者需要取值:簡(jiǎn)單的數(shù)組、鏈表只要保存當(dāng)前調(diào)用步驟(數(shù)組索引,
//或者當(dāng)前指針)和調(diào)用環(huán)境(內(nèi)部數(shù)據(jù)集)的結(jié)構(gòu),返回給用戶就可以了。//用戶每次調(diào)用iterator.next,iterator就把索引或指針向后移動(dòng)一下。 如果是內(nèi)部數(shù)據(jù)復(fù)雜的Tree,
//Graph結(jié)構(gòu),就相當(dāng)復(fù)雜了。比如是遍歷一棵樹,而且這棵樹的Node里面沒(méi)有Parent引用,那么Iterator
//必須自己維護(hù)一個(gè)棧把前面的所有的Parent Node都保存起來(lái)。typeTProducer =class privatei: integer; publicCurrent: Integer; function MoveNext: boolean;
end;
procedure TProducer.MoveNext;
var
i: integer;
beginResult := 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 beginConsumer(Current);.... //隨時(shí)可以停止從生產(chǎn)者取值 end;
finallyFree; end;
end;
這時(shí)候,有聰明人就將目光轉(zhuǎn)向了非對(duì)稱式(asymmetric-coroutine)協(xié)程。不難看出這里面的Iterator就是Coroutine里面的生產(chǎn)者(數(shù)據(jù)提供者,所以有時(shí)我們也稱之為Generator)。
一旦用戶(消費(fèi)者Consumer角色)調(diào)用了iterator.next(coroutine.Resume),Iterator就繼續(xù)向下執(zhí)行一步,然后把當(dāng)前遇到的內(nèi)部數(shù)據(jù)的Node放到一個(gè)消費(fèi)者用戶能夠看到的公用的緩沖區(qū)(比如,直接放到消費(fèi)者線程棧里面的局部變量)里面,然后自己就停下來(lái)(coroutine.Yield)。然后消費(fèi)者用戶就從緩沖區(qū)里面獲得了那個(gè)Node。這樣Iterator就可以自顧自地進(jìn)行遞歸運(yùn)算,不需要自己管理堆棧上下文,而是協(xié)程機(jī)制幫助它分配和管理運(yùn)行棧。從而實(shí)現(xiàn)將“控制”和“行為”的徹底解藕。請(qǐng)看如下C#程序:
using System.Collections.Generic;
public class MyProducer : IEnumerable<string>{ //iterator block,實(shí)現(xiàn)枚舉元素控制 public IEnumerator<string> GetEnumerator()
{
for(int i =0; i<elements.Length; i++)
yield elements[i];
}
...
}
foreach (string item innew MyProducer())
{
//實(shí)現(xiàn)終端上打印元素的行為 Console.WriteLine(item);
}
在這段代碼執(zhí)行過(guò)程中,foreach 的循環(huán)體和 GetEnumerator函數(shù)體實(shí)際上是在同一個(gè)線程中交替執(zhí)行的。這是一種介于線程和順序執(zhí)行之間的協(xié)同執(zhí)行模式,之所以稱之為協(xié)同(Coroutine),是因?yàn)橥瑫r(shí)執(zhí)行的多個(gè)代碼塊之間的調(diào)度是由邏輯隱式協(xié)同完成的。就協(xié)同執(zhí)行而言,從功能上可以分為行為、控制兩部分,控制又可進(jìn)一步細(xì)分為控制邏輯和控制狀態(tài)。行為對(duì)應(yīng)著如何處理目標(biāo)對(duì)象,如上述代碼中:行為就是將目標(biāo)對(duì)象打印到終端;控制則是如何遍歷這個(gè)elements數(shù)組,可進(jìn)一步細(xì)分為控制邏輯(順序遍歷)和控制狀態(tài)(當(dāng)前遍歷到哪個(gè)元素)。其中心思想在于通過(guò)Coroutine程序控制機(jī)制和Yield將其行為與控制徹底分離,以此來(lái)進(jìn)一步降低代碼的耦合度,增強(qiáng)通用性,提高代碼的復(fù)用率。
聯(lián)系客服