為什麼我認為goroutine和channel是把別的平臺上類庫的功能內建在語言裡
這幾天看了《Go語言程式設計》這本書,感覺一般,具體可見這篇書評。書評裡面我提到“Go語言的goroutine和channel其實是把別的語言/平臺上類庫的功能內建到語言裡”,這句話當然單單這麼說出來是沒什麼價值的,於是我也就趁熱把它說得再詳細一些。我的看法簡而言之是:由goroutine和channel所帶來的主要程式設計正規化、設計思路等等,其實基本都可以在其他一些平臺中配合特定的類庫來實現。
我們知道,作業系統的最小排程單元是“執行緒”,要執行任何一段程式碼,都必須落實到“執行緒”上。可惜執行緒太重,資源佔用太高,頻繁建立銷燬會帶來比較嚴重的效能問題,於是又誕生出執行緒池之類的常見使用模式。也是類似的原因,“阻塞”一個執行緒往往不是一個好主意,因為執行緒雖然暫停了,但是它所佔用的資源還在。執行緒的暫停和繼續對於排程器都會帶來壓力,而且執行緒越多,排程時的開銷便越大,這其中的平衡很難把握。
正因為如此,也有人提出並實現了fiber或coroutine這樣的東西,所謂fiber便是一個比執行緒更小的程式碼執行單位,假如說“執行緒”是用來計算的“物理”資源,那麼fiber就可以認為是計算的“邏輯”資源了。從理念上說,goroutine和WebWorker都是類似fiber或coroutine這樣的概念(所以叫做goroutine):它們都是執行邏輯的計算單元,我們可以建立大量此類單元而不用擔心佔用過多資源,自有排程器來使用一個或多個執行緒來執行它們的邏輯。
Go語言使用go
關鍵字來將任意一條語句放到一個coroutine上去執行。假如只是簡單地執行一段邏輯,那麼這和丟一段程式碼去執行緒池裡執行可以說沒有任何區別。但關鍵就在於,由於一個coroutine幾乎就是個普通的物件,因此我們往往可以放心地阻塞它的邏輯,一旦阻塞排程器可以讓當前執行緒立即去執行其他fiber上的程式碼。這裡的阻塞往往就是通過Go語言中的channel帶來的,一般來說會發生在“讀”和“寫”的時候:
func DoSomething(ch chan int) {
ch <- 1
var i = <-ch
}
上面程式碼中的ch
就是一個用來儲存int
型別資料的channel。第一行程式碼是向其寫入資料,可能在channel寫滿的時候阻塞。第二行則是從中獲取資料,在channel為空的時候阻塞。可以看出,所謂channel其實就是一個再簡單不過的容器而已。假如要類比.NET類庫,則可以認為它是一個實現了ITargetBlock
和ISourceBlock
的物件(例如一個BufferBlock
):
static async void DoSomething<T>(T block)
where T : ISourceBlock<int>, ITargetBlock<int> {
await block.SendAsync(1);
var i = await block.ReceiveAsync();
}
類似Go語言中的超時等特性自然也一應俱全。當然,這裡還並不能完全說是“類庫”,畢竟還用到了C# 5裡的async/await特性。我相信假如您對async/await有所瞭解的話,肯定也會聽到一些它跟coroutine相關或類比的聲音。它們在概念和效果上的確十分相似,當然背後的實現是有很大不同的。假如你一定要用coroutine,那還是免不了由語言或執行時提供支援。不過基於goroutine和channel的程式設計模式幾乎完全可以由類庫來實現。
在Go語言中,基於goroutine和channel的程式設計模式往往是這樣的:
func (ch chan int) {
for { // 死迴圈
var msg = <-ch
Process(msg)
}
}
這樣的“程式碼編寫模式”是基於阻塞的,這需要coroutine支援。不過假如我們把需求分析到最基礎的部分,它其實僅僅是:
- 可以建立大量佇列,每個佇列可以儲存大量任務。
- 單個佇列中的任務嚴格序列。
- 儘可能高效地(自然可以並行)處理系統中所有佇列裡的任務。
這就完全是類庫能實現的功能了,各個平臺上的此類成熟類庫並不少見:
- iOS上的GCD,或者說libdispatch。
- Java平臺上與GCD理念相同的HawtDispatch類庫。
- 與Scala語言關係更為密切的Akka類庫。
- .NET中的TPL Dataflow(之前提到的
BufferBlock
的出處)。
這些類庫與Go語言中基於goroutine和channel的開發方式有著相似的基礎,也完全有能力使用同樣的方式來架構系統。基於這些類庫,我們只需要提交大量的任務,至於這些任務什麼時候被執行則是內部實現所關心的問題,類庫自身將會把這些任務排程到物理執行緒上執行,用一種最高效,代價最低的方式。當然,我們也可以對這些佇列進行一些配置,這甚至比Go或Erlang中直接由語言執行時來提供的排程支援有更細緻的控制粒度。
我在工作中用過HawtDispatch和TPL Dataflow,也深刻體會到它們的價值。尤其是後者,我用TPL Dataflow實現的業務更為複雜,簡直可以說大大改善了我的工作品質,拿它來模仿之前的程式設計模式則可以是這樣的:
var block = new ActionBlock<int>(Process);
往這個block
物件裡塞入的任何物件都會使用Process
方法進行處理。當然TPL Dataflow的功能不止如此,它有著大量的高階功能,例如TransformBlock
可以在保證順序的情況下進行一對多的資料轉換,十分好用。具體內容可以參考這篇說明。
當然,像Go與Erlang這種對coroutine和併發直接提供支援的語言還可以有其他一些做法,例如Go可以做到先從channel A中獲取資料,然後在一個邏輯分支中再從channel B中獲取資料。這對於只提供任務佇列的類庫來說做起來就麻煩一些了(對於C#和Scala這類語言來說依然不成問題),不過在我的經驗裡這個限制似乎並不會成為嚴重的阻礙,我們依然可以實現相同訊息架構。
說起Erlang,其實在我看來它比Go的channel要好用不少。原因在於Erlang是動態型別語言,它的receive
操作可以用來匹配當前佇列(在Erlang裡叫做mailbox)中不同模式的元組,篩選出符合特定模式的訊息。與此相反,Go是靜態型別語言,它總是從一個channel中依次獲取型別相同的元素,這就完全類似於Java或C#中的泛型集合了。當然,這也不會是個影響系統設計的大問題。
說實話,我覺得這篇文章描述過多,但缺乏案例。其實我本來想通過改寫《Go語言程式設計》中的範例來說明問題,但後來發現書中關於channel和goroutine的例子實在太簡單了,沒法體現出一個這個特性所帶來“架構設計”。所以,示例什麼的找機會再說吧。
相關文章
- PHP為什麼會被認為是草根語言?PHP
- JAVA語言為什麼能跨平臺?Java
- 為什麼我最喜歡的程式語言是 GoGo
- 為什麼有人說中文是世界上最好的語言?
- 為什麼國內的公司都不敢使用Flex作為產品的平臺與主要開發語言薦Flex
- 為什麼我認為《變數》是最好的塔防之一變數
- 我為什麼反對語言之爭?我的語言歷險
- 為什麼自制指令碼語言是程式語言的最高境界?指令碼
- 為什麼我喜歡 Lisp 程式語言Lisp
- 我們為什麼要使用GO語言?Go
- 為什麼我們需要一門新語言——Go語言Go
- 松本行弘:我為什麼要開發新語言Streem(上)
- python和c語言的區別是什麼PythonC語言
- 為什麼《七週七語言》選中的是這幾種語言?
- 為什麼我認為Flutter是移動應用程式開發的未來Flutter
- 我為什麼認為Flutter是移動應用程式開發的未來?Flutter
- 評: 為什麼我不喜歡Go語言式的介面Go
- 為什麼需要更多的程式語言
- 何為程式語言?為什麼要學C語言?C語言
- 為什麼低程式碼平臺的認可度普遍較低
- 為什麼我不推薦 JavsScript 為首選程式語言
- 為什麼我不推薦JavsScript為首選程式語言
- 為什麼說php是最糟糕的,也是最好的程式語言PHP
- 為什麼Go是一種設計糟糕的程式語言Go
- 為什麼我是世界上最好的程式設計師?程式設計師
- 為什麼我喜歡富於表達性的程式語言
- 為什麼使用者是眾籌平臺的致命死穴?
- 《DARQ》開發者:我為什麼拒絕平臺獨佔?
- java內部類,為什麼需要內部類?Java
- Python和C語言區別是什麼?PythonC語言
- 為什麼我們喜歡看別人在遊戲裡受苦遊戲
- Python是什麼型別語言?為何Python這麼多人學習?Python型別
- Python是什麼?Python成為熱門語言的原因!Python
- Python成為爬蟲常用語言的原因是什麼?Python爬蟲
- 曾經我認為C語言就是個弟弟C語言
- 為什麼要學習和使用C語言?C語言
- 為什麼會有這麼多的程式語言?
- 為你的 Python 平臺類遊戲新增跳躍功能Python遊戲