Lua 的多執行緒支援
單個 Lua 虛擬機器只能工作在一個執行緒下,如果你需要在同一個程序中讓 Lua 並行處理一些事務,必須為每個執行緒部署獨立的 Lua 虛擬機器。
ps. 在少量多執行緒應用環境,加鎖也是可行的。你可以在編譯時自定義lua_lock(L)和lua_unlock(L)去呼叫作業系統的鎖。
比較成熟的 lua 多執行緒庫有ofollow,noindex" target="_blank">Lanes 和Effil 。它們都試圖隱藏多虛擬機器的細節,讓使用者使用起來好像多執行緒在使用同一個虛擬機器一樣。比如 Effil 就用了 effil.table 去模擬 table 並讓多個虛擬機器可以共享資料;Lanes 則有 deep userdata 可以在不同執行緒間共享。
這些個方案都允許使用者在不同的虛擬機器間相互呼叫函式,大約是利用在虛擬機器間同步函式的位元組碼和 upvalue 實現的。基於這些執行緒庫的程式,用起來和別的支援多執行緒的語言(比如 Golang)那樣沒有太大區別。
但我不喜歡這種多執行緒解決方案。我認為多執行緒本身是複雜的,隱藏執行緒並行執行的細節,讓使用者基於共享狀態去寫程式並沒有什麼好處。如果閱讀程式碼的人一眼看上去並不能立刻分辨出一個函式到底會在哪個執行緒執行,只會增加多執行緒程式的維護成本。
所以,skynet 採用的是讓框架去支援多執行緒,讓使用者明確知道有獨立的虛擬機器的概念,以及它們跑在不同的執行緒下。只用訊息佇列來協同工作。還有很多類似的專案也是這樣做的,比如cqueues 。
我最近在開發客戶端引擎時也遇到了多執行緒問題。例如 bgfx 的 log 回撥就可能發生在不同執行緒中,所以無法簡單的封裝成 lua 函式進行回撥;我們的引擎需要通過網路載入資源/程式碼,這部分網路 IO 處理最好能和邏輯執行緒分離,等等。
一開始,我採用的是 Lanes 。但是做了一段時間後,我覺得濫用多執行緒很容易滋生 bug 。
最近,我希望去掉 Lanes 這個庫,改用自己實現的一套最簡的執行緒庫。由於客戶端執行緒數量比較固定,無非是渲染執行緒,邏輯執行緒,IO 執行緒,Lua 偵錯程式執行緒,物理執行緒;執行緒之間有固定的訊息管道交換資料就夠了。
所以,我想我可以從最基本的執行緒 api 設計起,一點點按需完善這個執行緒庫。一開始,只需要有執行緒的建立;通訊管道可以支援收發 lua 基礎資料型別就可用了。這部分在 skynet 中已經非常穩定,只需要把程式碼搬過來。
由於圖形客戶端有天然的執行週期(按幀渲染),我甚至不需要給通訊管道加讀取超時引數。只用週期性查詢是否有新資料即可。通訊管道也是有限的,所以並不需要像 golang 那樣把 channel 做成 first class 的型別,並支援自由建立和銷燬。我可以支援有限數量的具名管道,不同執行緒間約定好名字就可以通訊。
這些基礎設施實現出來比我想象的要簡單。搬運一些老程式碼,新增好 lua 的封裝,一個週末就完成了。下週可以基於它們來重構我們已經做好(但還有 bug )的引擎程式碼。