Lua的多任務(wù)機(jī)制——協(xié)程(coroutine)




   并發(fā)是現(xiàn)實(shí)世界的本質(zhì)特征,而聰明的計(jì)算機(jī)科學(xué)家用來模擬并發(fā)的技術(shù)手段便是多任務(wù)機(jī)制。大致上有這么兩種多任務(wù)技術(shù),一種是搶占式多任務(wù) (preemptive multitasking),它讓操作系統(tǒng)來決定何時(shí)執(zhí)行哪個(gè)任務(wù)。另外一種就是協(xié)作式多任務(wù)(cooperative multitasking),它把決定權(quán)交給任務(wù),讓它們在自己認(rèn)為合適的時(shí)候自愿放棄執(zhí)行。這兩種多任務(wù)方式各有優(yōu)缺點(diǎn),前者固有的同步問題使得程序經(jīng) 常有不可預(yù)知的行為,而后者則要求任務(wù)具備相當(dāng)?shù)淖月删瘛?br />    協(xié)程(coroutine)技術(shù)是一種程序控制機(jī)制,早在上世紀(jì)60年代就已 提出,用它可以很方便地實(shí)現(xiàn)協(xié)作式多任務(wù)。在主流的程序語言(如C++、Java、Pascal等)里我們很少能看到協(xié)程的身影,但是現(xiàn)在不少動(dòng)態(tài)腳本語 言(Python、Perl)卻都提供了協(xié)程或與之相似的機(jī)制,其中最突出的便是Lua。
  
    Lua語言實(shí)現(xiàn)的協(xié)程是一種非對(duì)稱 式(asymmetric)協(xié)程,或稱半對(duì)稱式(semi-asymmetric)協(xié)程,又或干脆就叫半?yún)f(xié)程(semi-coroutine)。這種協(xié)程 機(jī)制之所以被稱為非對(duì)稱的,是因?yàn)樗峁┝藘煞N傳遞程序控制權(quán)的操作:一種是(重)調(diào)用協(xié)程(通過coroutine.resume);另一種是掛起協(xié)程 并將程序控制權(quán)返回給協(xié)程的調(diào)用者(通過coroutine.yield)。一個(gè)非對(duì)稱協(xié)程可以看做是從屬于它的調(diào)用者的,二者的關(guān)系非常類似于例程 (routine)與其調(diào)用者之間的關(guān)系。既然有非對(duì)稱式協(xié)程,當(dāng)然也就有對(duì)稱式(symmetric)協(xié)程了,它的特點(diǎn)是只有一種傳遞程序控制權(quán)的操 作,即將控制權(quán)直接傳遞給指定的協(xié)程。曾經(jīng)有這么一種說法,對(duì)稱式和非對(duì)稱式協(xié)程機(jī)制的能力并不等價(jià),但事實(shí)上很容易根據(jù)前者來實(shí)現(xiàn)后者。接下來我們就用 代碼來證明這個(gè)事實(shí)。
  
  --對(duì)稱式協(xié)程庫coro.lua
  
  coro = {}
  --coro.main用來標(biāo)識(shí)程序的主函數(shù)
  coro.main = function() end
  -- coro.current變量用來標(biāo)識(shí)擁有控制權(quán)的協(xié)程,
  -- 也即正在運(yùn)行的當(dāng)前協(xié)程
  coro.current = coro.main
  
  -- 創(chuàng)建一個(gè)新的協(xié)程
  function coro.create(f)
    return coroutine.wrap(function(val)
                return nil,f(val)
               end)
  end
  
  -- 把控制權(quán)及指定的數(shù)據(jù)val傳給協(xié)程k
  function coro.transfer(k,val)
    if coro.current ~= coro.main then
     return coroutine.yield(k,val)
    else
     -- 控制權(quán)分派循環(huán)
     while k do
       coro.current = k
       if k == coro.main then
        return val
       end
       k,val = k(val)
     end
     error("coroutine ended without transfering control...")
    end
  end
  
  如果暫時(shí)還弄不懂上面的程序,沒關(guān)系,看看如何使用這個(gè)庫后再回頭分析。下面是使用示例:
  
  require("coro.lua")
  
  function foo1(n)
    print("1: foo1 received value "..n)
    n = coro.transfer(foo2,n + 10)
    print("2: foo1 received value "..n)
    n = coro.transfer(coro.main,n + 10)
    print("3: foo1 received value "..n)
    coro.transfer(coro.main,n + 10)
  end
  
  function foo2(n)
    print("1: foo2 received value "..n)
    n = coro.transfer(coro.main,n + 10)
    print("2: foo2 received value "..n)
    coro.transfer(foo1,n + 10)
  end
  
  function main()
    foo1 = coro.create(foo1)
    foo2 = coro.create(foo2)
    local n = coro.transfer(foo1,0)
    print("1: main received value "..n)
    n = coro.transfer(foo2,n + 10)
    print("2: main received value "..n)
    n = coro.transfer(foo1,n + 10)
    print("3: main received value "..n)
  end
  
  --把main設(shè)為主函數(shù)(協(xié)程)
  coro.main = main
  --將coro.main設(shè)為當(dāng)前協(xié)程
  coro.current = coro.main
  --開始執(zhí)行主函數(shù)(協(xié)程)
  coro.main()
  
  
   上面的示例定義了一個(gè)名為main的主函數(shù),整個(gè)程序由它而始,也因它而終。為什么需要一個(gè)這樣的主函數(shù)呢?上面說了,程序控制權(quán)可以在對(duì)稱式協(xié)程之間 自由地直接傳遞,它們之間無所謂誰從屬于誰的問題,都處于同一個(gè)層級(jí),但是應(yīng)用程序必須有一個(gè)開始點(diǎn),所以我們定義一個(gè)主函數(shù),讓它點(diǎn)燃程序運(yùn)行的導(dǎo)火 線。雖說各個(gè)協(xié)程都是平等的,但做為程序運(yùn)行原動(dòng)力的主函數(shù)仍然享有特殊的地位(這個(gè)世上哪有絕對(duì)的平等!),為此我們的庫專門用了一個(gè) coro.main變量來保存主函數(shù),并且在它執(zhí)行之前要將它設(shè)為當(dāng)前協(xié)程(雖然上面的main實(shí)際只是一個(gè)普通函數(shù)而非一個(gè)真正的協(xié)程,但這并無太大的 關(guān)系,以后主函數(shù)也被稱為主協(xié)程)。示例運(yùn)行的結(jié)果是:
  
  1: foo1 received value 0
  1: foo2 received value 10
  1: main received value 20
  2: foo2 received value 30
  2: foo1 received value 40
  2: main received value 50
  3: foo1 received value 60
  3: main received value 70
  
  協(xié)程的執(zhí)行序列是:main->foo1->foo2->main->foo2->foo1->main->foo1->main。
  
     coro.transfer(k,val)函數(shù)中k是將要接收程序控制權(quán)的協(xié)程,而val是傳遞給k的數(shù)據(jù)。如果當(dāng)前協(xié)程不是主協(xié)程, tansfer(k,val)就簡單地利用coroutine.yield(k,val)將當(dāng)前協(xié)程掛起并傳回兩項(xiàng)數(shù)據(jù),即程序控制權(quán)的下一站和傳遞給它 的數(shù)據(jù);否則進(jìn)入一個(gè)控制權(quán)分派(dispatch)循環(huán),該循環(huán)(重)啟動(dòng)(resume)k協(xié)程,等待它執(zhí)行到掛起(suspend),并根據(jù)此時(shí)協(xié) 程傳回的數(shù)據(jù)來決定下一個(gè)要(重)啟動(dòng)的協(xié)程。從應(yīng)用示例來看,協(xié)程與協(xié)程之間似乎是用transfer直接傳遞控制權(quán)的,但實(shí)際上這個(gè)傳遞還是通過了主 協(xié)程。每一個(gè)在主協(xié)程里被調(diào)用(比較coro.current和coro.main是否相同即可判斷出)的transfer都相當(dāng)于一個(gè)協(xié)程管理器,它不 斷地(重)啟動(dòng)一個(gè)協(xié)程,將控制權(quán)交出去,然后等那個(gè)協(xié)程掛起時(shí)又將控制權(quán)收回,然后再(重)啟動(dòng)下一個(gè)協(xié)程...,這個(gè)動(dòng)作不會(huì)停止,除非< 1>將(重)啟動(dòng)的協(xié)程是主協(xié)程;<2>某個(gè)協(xié)程沒有提供控制權(quán)的下一個(gè)目的地。很顯然,每一輪分派循環(huán)開始時(shí)都由主協(xié)程把握控制權(quán), 在循環(huán)過程中如果控制權(quán)的下一站又是主協(xié)程的話就意味著這個(gè)當(dāng)初把控制權(quán)交出去的主協(xié)程transfer操作應(yīng)該結(jié)束了,所以函數(shù)直接返回val從而結(jié)束 這輪循環(huán)。對(duì)于情況<2>,因?yàn)閏oro.create(f)創(chuàng)建的協(xié)程的體函數(shù)(body function)實(shí)際是function(val) return nil,f(val) end,所以當(dāng)函數(shù)f的最后一條指令不是transfer時(shí),這個(gè)協(xié)程終將執(zhí)行完畢并把nil和函數(shù)f的返回值一起返回。如果k是這樣的協(xié)程, transfer執(zhí)行完k,val = k(val)語句后k值就成了nil,這被視為一個(gè)錯(cuò)誤,因?yàn)槌绦虼藭r(shí)沒法確定下一個(gè)應(yīng)該(重)啟動(dòng)的協(xié)程到底是誰。所以在對(duì)稱式模型下,每一個(gè)協(xié)程(當(dāng) 然主協(xié)程出外)最后都必須顯式地將控制權(quán)傳遞給其它的協(xié)程。根據(jù)以上分析,應(yīng)用示例的控制權(quán)的分派應(yīng)為:
  
  第一輪分派: main->foo1->main->foo2->main->main(結(jié)束)
  第二輪分派: main->foo2->main->foo1->main->main(結(jié)束)
  第三輪分派: main->foo1->main->main(結(jié)束)
  
     由于可以直接指定控制權(quán)傳遞的目標(biāo),對(duì)稱式協(xié)程機(jī)制擁有極大的自由,但得到這種自由的代價(jià)卻是犧牲程序結(jié)構(gòu)。如果程序稍微復(fù)雜一點(diǎn),那么即使是非常 有經(jīng)驗(yàn)的程序員也很難對(duì)程序流程有全面而清晰的把握。這非常類似goto語句,它能讓程序跳轉(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)用者,這與計(jì)算機(jī)先哲們倡導(dǎo)的結(jié)構(gòu)化編程風(fēng)格完全一致。
  
    綜上所述,Lua提供的非對(duì)稱式協(xié)程不但具有與對(duì)稱式協(xié)程一樣強(qiáng)大的能力,而且還能避免程序員濫用機(jī)制寫出結(jié)構(gòu)混亂的程序。