ConcurrentLua--面向併發的Lua編程
原文地址Linker 翻譯此文只爲提供更多信息.
介紹
ConcurrentLua 是一個無共享異步消息傳遞模型的實現.該模型來自Erlang語言.她改編了Erlang的併發元素並整合進Lua裏.
ConcurrentLua的一個核心元素是 process(進程).一個進程是一個輕量級虛擬機
線程,扮演和操作系統的進程同樣的角色;他們不共享內存而是使用某這進程間通訊
機制.這些進程能夠根據需要被創建和銷燬,並通過一個簡單的環羅賓(輪訓)算法來
調度他們.
每一個進程關聯到一個郵箱(臨時存儲消息的隊列),通過郵箱來接受別的進程發來
的消息.進程可以在任何時候檢查自己的郵箱有沒有新消息抵達,如果有,進程可以
按照抵達的順序依次讀取.
每個進程都有一個唯一的數字作爲進程標識,叫 PID(process identifier).也可以
給進程取一個名字,並用名字來指代進程.進程名和進程的對應關係被保存到一個
中心儲藏室--registry(註冊表).進程可以編輯註冊表,添加或者刪除表項.
錯誤捕捉機制也被實現成 monitors 和 links .通過 monitors 進程能夠監視其他
進程,並在被監視進程異常終止的時候獲得通知.通過 linker 進程綁定進程到一起,
當一個進程異常終止的時候,其他進程也被通知並終止.
本系統還支持分佈式編程和所有相關的組件.分佈式的進程通過和本地進程一樣的
方式來通訊.
分佈是基於 node(節點) 組件的.一個節點代表一個運行着很多進程的運行時環境.
節點們可以相互連接和通訊,於是建立了一個虛擬網絡.分佈的進程使用這個網絡來
順序交換信息.
每個節點有個名字.其他的節點可以通過這個名字來連接.一個端口映射器精靈進程
(譯註:就是服務進程,類似Erlang提供的名字服務)提供了名字解析服務.端口映射器
知曉虛擬網絡中所有的節點的信息.
正如進程可以在本地創建,進程已可以在遠端節點被創建.一個遠程進程能夠被視同
爲一個本地進程來操作.
如果虛擬網絡中的節點是全互聯的(每一個節點雙向連接其他的節點),那麼可以使用
全局進程名.節點們相互交流和保養虛擬全局註冊表並保持自己本地的註冊表時時
更新.
monitors 和 links 以同樣的語義支持分佈進程和本地進程.節點可以透明的處理
分佈進程的錯誤.另外,進程可以像監視整個節點.
節點可以在通訊前進行鑑權.一個已鑑權的節點纔可以成爲虛擬網絡的一部分.這些
策略通過一個簡單安全機制來保證.
實現
ConcurrentLua的實現是基於Lua組件系統實現的.這個系統負責組織和管理Lua的模塊和子模塊.主模塊有兩個,分別提供了併發功能和分佈式編程功能.併發模塊
可以單獨加載,每個模塊都可以選擇性加載需要使用的子模塊.獨立的端口映射器
精靈進程也是實現的一部分.
系統中的進程是通過Lua的協程機制來實現的.一個進程其實就是一個Lua協程,
通過 yield 來掛起一個進程,通過 resume 來繼續執行一個進程.
進程的調度機制仍然是基於Lua使用的 協作式多線程模型. 進程自願掛起自己,
從而讓其它的進程獲得運行的機會.然而,掛起和恢復進程被部分隱藏於高層機制
之下;當一個進程去等待消息抵達的時候掛起,而在消息抵達進程郵箱後準備恢復.
一個簡單的環羅賓(輪訓)調度器用來恢復進程的執行.
任何類型的Lua數據,除了內存引用外,都可以通過消息來發送.消息可以是布爾值,
數字,字符串,表或者函數,或者他們的混合.數據自動在發送時被序列化,並在接受
時反序列化,所以的數據都是值傳遞.
節點間的分佈式進程間通訊機制是基於異步socket的.映射到網絡層是非阻塞
socket和定時輪訓.這是如今大部分Lua模塊採用的方法,非阻塞語義也應該被用
在例如文件和管道的IO操作上.
用法
一些例子提供了系統不要組件的用法,例如,創建進程,分佈式進程的消息傳遞和錯誤捕獲.創建進程
spawn()函數可以創建進程.spawn()函數接受至少一個參數,該參數標誌進程的入口函數.其它附加參數則被直接轉交給入口函數.
下面的例子示範創建一個進程.該進程輸出指定次數的消息:
require 'concurrent' function hello_world(times) for i = 1, times do print('hello world') end print('done') end concurrent.spawn(hello_world, 3) concurrent.loop() |
輸出應該是:
hello world hello world hello world done |
首先加載系統:
require 'concurrent'
進程入口函數:
function hello_world(times)
for i = 1, times do print('hello world') end
print('done')
end
創建一個新進程:
concurrent.spawn(hello_world, 3)
最後調用系統無限循環:
concurrent.loop()
消息交互
進程通過 send() 和 receive() 函數來交換消息.同樣,self()函數也被用來獲取本進程ID.下面的程序實現了兩個進程交換消息然後終止:
require 'concurrent' function pong() while true do local msg = concurrent.receive() if msg.body == 'finished' then break elseif msg.body == 'ping' then print('pong received ping') concurrent.send(msg.from, { body = 'pong' }) end end print('pong finished') end function ping(n, pid) for i = 1, n do concurrent.send(pid, { from = concurrent.self(), body = 'ping' }) local msg = concurrent.receive() if msg.body == 'pong' then print('ping received pong') end end concurrent.send(pid, { from = concurrent.self(), body = 'finished' }) print('ping finished') end pid = concurrent.spawn(pong) concurrent.spawn(ping, 3, pid) concurrent.loop() |
輸出應該是:
pong received ping ping received pong pong received ping ping received pong pong received ping ping received pong pong finished ping finished |
在 pong 進程被創建後, ping 進程獲得了 pong 進程的 PID:
pid = concurrent.spawn(pong)
concurrent.spawn(ping, 3, pid)
ping 進程發送一個消息:
concurrent.send(pid, {
from = concurrent.self(),
body = 'ping'
})
pong 進程等待消息抵達,然後把接收到的消息保存到一個變量中:
local msg = concurrent.receive()
pong 進程回覆:
concurrent.send(msg.from, { body = 'pong' })
pong 進程在接收到 ping 進程發來的一個提示後終結.
註冊進程名
可以用進程名替代PID來指定消息接收方. register() 函數可以用來在註冊表(譯註:指系統的名字對應表,而不是Windows的註冊表,順便鄙視一下Windows. :) )
創建一個進程的名字:
require 'concurrent' function pong() while true do local msg = concurrent.receive() if msg.body == 'finished' then break elseif msg.body == 'ping' then print('pong received ping') concurrent.send(msg.from, { body = 'pong' }) end end print('pong finished') end function ping(n) for i = 1, n do concurrent.send('pong', { from = concurrent.self(), body = 'ping' }) local msg = concurrent.receive() if msg.body == 'pong' then print('ping received pong') end end concurrent.send('pong', { from = concurrent.self(), body = 'finished' }) print('ping finished') end pid = concurrent.spawn(pong) concurrent.register('pong', pid) concurrent.spawn(ping, 3) concurrent.loop() |
相對前一個版本的改變就是 ping 進程發送消息的地方:
concurrent.send('pong', {
from = concurrent.self(),
body = 'ping'
})
和:
concurrent.send('pong', {
from = concurrent.self(),
body = 'finished'
})
以及現在 pong 進程註冊了它的名字:
concurrent.register('pong', pid)
因此 ping 進程不需要知道 pong 進程的 PID 了.
分佈式消息傳遞
不同節點上的進程仍然可以使用同樣的消息傳遞機制.遠程進程通過 PID或進程名 加上節點名來指定.先前的例子可以改造成兩個程序,分別是一個獨立進程.
pong 進程的代碼如下:
require 'concurrent' function pong() while true do local msg = concurrent.receive() if msg.body == 'finished' then break elseif msg.body == 'ping' then print('pong received ping') concurrent.send(msg.from, { body = 'pong' }) end end print('pong finished') end concurrent.init('pong@gaia') pid = concurrent.spawn(pong) concurrent.register('pong', pid) concurrent.loop() concurrent.shutdown() |
ping 進程的代碼如下:
require 'concurrent' function ping(n) for i = 1, n do concurrent.send({ 'pong', 'pong@gaia' }, { from = { concurrent.self(), concurrent.node() }, body = 'ping' }) local msg = concurrent.receive() if msg.body == 'pong' then print('ping received pong') end end concurrent.send({ 'pong', 'pong@gaia' }, { from = { concurrent.self(), concurrent.node() }, body = 'finished' }) print('ping finished') end concurrent.spawn(ping, 3) concurrent.init('ping@selene') concurrent.loop() concurrent.shutdown() |
(譯註: 如果你想自己跑這個例子需要修改上面的節點名後半部分的機器名部分,使之和你的網絡環境相匹配.)
pong 進程的輸出應該是:
pong received ping
pong received ping
pong received ping
pong finished
ping 進程的輸出應該是:
ping received pong
ping received pong
ping received pong
ping finished
在這個例子裏,運行時系統運行在分佈式模式.爲了看到結果,端口映射器必須先運行:
$ clpmd
初始化 pong 進程所在節點的代碼:
concurrent.init('pong@gaia')
初始化 ping 進程所在節點的代碼:
concurrent.init('ping@selene')
上面兩句代碼註冊節點到端口映射器.去註冊是通過:
concurrent.shutdown()
這個例子的唯一改動是消息發送的目的地.node()函數會返回調用進程坐在節點的名字:
concurrent.send({ 'pong', 'pong@gaia' }, {
from = { concurrent.self(), concurrent.node() },
body = 'ping'
})
接下來:
concurrent.send({ 'pong', 'pong@gaia' }, {
from = { concurrent.self(), concurrent.node() },
body = 'finished'
})
錯誤處理
一個捕獲進程間錯誤的方法是連接進程.兩個進程被綁定到一起,一個異常終止的後另一個也會終止.link()函數用來綁定進程:
require 'concurrent' function ping(n, pid) concurrent.link(pid) for i = 1, n do concurrent.send(pid, { from = concurrent.self(), body = 'ping' }) local msg = concurrent.receive() if msg.body == 'pong' then print('ping received pong') end end print('ping finished') concurrent.exit('finished') end function pong() while true do local msg = concurrent.receive() if msg.body == 'ping' then print('pong received ping') concurrent.send(msg.from, { body = 'pong' }) end end print('pong finished') end pid = concurrent.spawn(pong) concurrent.spawn(ping, 3, pid) concurrent.loop() |
輸出應該是:
pong received ping
ping received pong
pong received ping
ping received pong
pong received ping
ping received pong
pong finished -- 譯註:這裏應該是: ping fininshed
pong 進程永遠不會運行到最後一行,因爲他在接收到 ping 進程退出信號的時候會終止.
連接進程的代碼如下:
concurrent.link(pid)
也可以捕獲進程終止導致的exit信號.被捕獲的exit信號會轉換成一個特殊的消息:
require 'concurrent' concurrent.setoption('trapexit', true) function pong() while true do local msg = concurrent.receive() if msg.signal == 'EXIT' then break elseif msg.body == 'ping' then print('pong received ping') concurrent.send(msg.from, { body = 'pong' }) end end print('pong finished') end function ping(n, pid) concurrent.link(pid) for i = 1, n do concurrent.send(pid, { from = concurrent.self(), body = 'ping' }) local msg = concurrent.receive() if msg.body == 'pong' then print('ping received pong') end end print('ping finished') concurrent.exit('finished') end pid = concurrent.spawn(pong) concurrent.spawn(ping, 3, pid) concurrent.loop() |
輸出應該是:
pong received ping
ping received pong
pong received ping
ping received pong
pong received ping
ping received pong
pong finished
ping finished
可以通過 setoption() 函數來設置進程鏈接的選項,這裏是 trapexit 選項:
concurrent.setoption('trapexit', true)
pong 進程會接收到一個退出消息:
if msg.signal == 'EXIT' then
break
基於提示消息的monitor, 也可以用來處理錯誤.