最近和同事聊起來,覺得lua缺乏編譯型語言的類型校驗功能,還有變量拼寫檢查之類的,導致線上總是有低級錯誤出現。比如最近有一個是變量名拼寫少了一個字母,導致某功能沒開啟;還有一個是變量傳參時,之前測試多加了一個參數,測試完成后忘記刪了,導致參數順序不對。之前看過有個TypeLua,沒想到現在已經不怎么維護了,去搞了Titan-lang,然而Titan也是4個月沒動靜了。。。
目前也沒想到什么好的解決辦法,於是順帶看看Rust-lang,開闊一下思路。我最佩服rust這幫人的,是他們有點壯士斷腕的勇氣,已經寫好的runtime和協程,認為和rust的定位不妥,就直接砍掉了。相關的PR在這里,rfcs在這里。rfcs里面關於協程、線程模型的討論相當到位,我簡要翻譯一下,權當記錄。以下是譯文(節譯):
背景:線程/任務模型和I/O
很多語言/庫會提供任務,任務一般有別於操作系統的原始線程。任務的特性可以按以下幾個維度區分:
- 1:1 vs M:N 最根本的問題是一個任務是不是總是對應到一個OS級別的線程(1:1模型),或者是通過用戶空間調度器,將任務映射到worker線程上(M:N模型)。有一些內核,比如Windows,支持用戶空間調度的1:1模型,結合了這兩者的優勢。
在M:N模型里,關於任務何時或者是否在worker線程遷移,各自有不同的選擇。但是這個模型有個弊端,如果其中一個任務觸發了page fault,整個worker線程都會被阻塞。選擇合適的worker線程數目是非常困難的,有些框架嘗試動態分配線程數,這也會產生額外開銷。
- 棧管理。在1:1模型里,任務就是線程,天然具有棧。在M:N模型里,任務可能有他們自己的棧,這里有重要的取舍:
- 分段棧(segmented stacks)允許棧隨着時間增長,意味着任務可以有自己的棧,而且依然保持輕量。但是,分段棧有明顯的性能問題和復雜度開銷。
- 在沒有自己的棧的情況下,任務要么無法在工作線程間遷移(例如Java框架里的fork/join),或者只能用CPS(continuation-passing style)實現,即每個阻塞操作都用一個閉包保存自己的工作狀態。(CPS一般將用到的棧保存在閉包里)好處是這些任務特別輕量,基本只是閉包的開銷。
- 阻塞和I/O支持。在1:1模型里,一個任務可以被任意阻塞而不會影響其他任務,因為每個任務都是一個OS線程。在M:N模型里,OS的阻塞意味着工作線程的阻塞(比如運行較長時間的循環,或者page fault)M:N模型可以用數種方法解決阻塞。在Java的fork/join框架下,是透過動態增減工作線程來實現的。另一種實現,是提供特殊的任務阻塞操作(包括I/O),將阻塞操作化作底層的非阻塞操作,允許底層工作線程繼續運行。但是,這種實現只能對顯式阻塞起作用,對於循環、page fault之類的就無效了。
Rust的現狀
Rust從綠色線程模型(即協程模型)轉向了原始線程模型:
- 在Rust的綠色線程模型里,任務是按M:N進行調度,有自己的棧。最初,Rust使用了分段棧,后面改成了預分配的棧,這樣Rust的綠色線程就不是輕量級的了。對阻塞的操作下文再敘
- 在Rust的原始線程模型里,任務是1:1的和OS線程匹配的。
(節略)
問題
強制的共同演進:綠色線程模型和原始線程模型必須提供相同的I/O接口,但是有部分接口只會在其中一種模型里有效。比如,輕量級
開銷:目前的Rust模型允許運行時將綠色線程模型和原始線程模型混合使用。但是實現上有如下缺點:
- 二進制大小。任意二進制文件里都包含了整個I/O系統的實現,因為他是libstd標准庫的一部分。
- 任務局部存儲。目前的任務局部存儲是可以無縫在原始線程和綠色線程間切換的。但是性能會有影響,即使可以改進,也比直接采用原始的線程局部存儲要復雜得多。
- 動態分配和調度。當前的設計下,所有I/O操作都需要動態調度,大部分的內存分配操作也是。但是,絕大部分情況下,
問題重重的I/O交互:
嵌入式的Rust:
維護困難: