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