Go 語言爲什麼這麼快,帶你詳細瞭解Golang CSP併發模型

go語言的最大兩個亮點,一個是goroutine,一個就是chan了。二者合體的典型應用CSP,基本就是大家認可的並行開發神器,簡化了並行程序的開發難度,我們來看一下CSP。

一、CSP是什麼

CSP 是 Communicating Sequential Process 的簡稱,中文可以叫做通信順序進程,是一種併發編程模型,是一個很強大的併發數據模型,是上個世紀七十年代提出的,用於描述兩個獨立的併發實體通過共享的通訊 channel(管道)進行通信的併發模型。相對於Actor模型,CSP中channel是第一類對象,它不關注發送消息的實體,而關注與發送消息時使用的channel。

嚴格來說,CSP 是一門形式語言(類似於 ℷ calculus),用於描述併發系統中的互動模式,也因此成爲一衆面向併發的編程語言的理論源頭,並衍生出了 Occam/Limbo/Golang…

而具體到編程語言,如 Golang,其實只用到了 CSP 的很小一部分,即理論中的 Process/Channel(對應到語言中的 goroutine/channel):這兩個併發原語之間沒有從屬關係, Process 可以訂閱任意個 Channel,Channel 也並不關心是哪個 Process 在利用它進行通信;Process 圍繞 Channel 進行讀寫,形成一套有序阻塞和可預測的併發模型。

二、Golang CSP

與主流語言通過共享內存來進行併發控制方式不同,Go 語言採用了 CSP 模式。這是一種用於描述兩個獨立的併發實體通過共享的通訊 Channel(管道)進行通信的併發模型。

Golang 就是借用CSP模型的一些概念爲之實現併發進行理論支持,其實從實際上出發,go語言並沒有,完全實現了CSP模型的所有理論,僅僅是借用了 process和channel這兩個概念。process是在go語言上的表現就是 goroutine 是實際併發執行的實體,每個實體之間是通過channel通訊來實現數據共享。

Go語言的CSP模型是由協程Goroutine與通道Channel實現:

  • Go協程goroutine: 是一種輕量線程,它不是操作系統的線程,而是將一個操作系統線程分段使用,通過調度器實現協作式調度。是一種綠色線程,微線程,它與Coroutine協程也有區別,能夠在發現堵塞後啓動新的微線程。

  • 通道channel: 類似Unix的Pipe,用於協程之間通訊和同步。協程之間雖然解耦,但是它們和Channel有着耦合。

三、Channel

Goroutine 和 channel 是 Go 語言併發編程的 兩大基石。Goroutine 用於執行併發任務,channel 用於 goroutine 之間的同步、通信。

Channel 在 gouroutine 間架起了一條管道,在管道里傳輸數據,實現 gouroutine 間的通信;由於它是線程安全的,所以用起來非常方便;channel 還提供 “先進先出” 的特性;它還能影響 goroutine 的阻塞和喚醒。

相信大家一定見過一句話:

Do not communicate by sharing memory; instead, share memory by communicating.

不要通過共享內存來通信,而要通過通信來實現內存共享。

這就是 Go 的併發哲學,它依賴 CSP 模型,基於 channel 實現。

channel 實現 CSP

Channel 是 Go 語言中一個非常重要的類型,是 Go 裏的第一對象。通過 channel,Go 實現了通過通信來實現內存共享。Channel 是在多個 goroutine 之間傳遞數據和同步的重要手段。

使用原子函數、讀寫鎖可以保證資源的共享訪問安全,但使用 channel 更優雅。

channel 字面意義是 “通道”,類似於 Linux 中的管道。聲明 channel 的語法如下:

chan T // 聲明一個雙向通道
chan<- T // 聲明一個只能用於發送的通道
<-chan T // 聲明一個只能用於接收的通道COPY

單向通道的聲明,用 <- 來表示,它指明通道的方向。你只要明白,代碼的書寫順序是從左到右就馬上能掌握通道的方向是怎樣的。

因爲 channel 是一個引用類型,所以在它被初始化之前,它的值是 nil,channel 使用 make 函數進行初始化。可以向它傳遞一個 int 值,代表 channel 緩衝區的大小(容量),構造出來的是一個緩衝型的 channel;不傳或傳 0 的,構造的就是一個非緩衝型的 channel。

兩者有一些差別:非緩衝型 channel 無法緩衝元素,對它的操作一定順序是 “發送 -> 接收 -> 發送 -> 接收 -> ……”,如果想連續向一個非緩衝 chan 發送 2 個元素,並且沒有接收的話,第一次一定會被阻塞;對於緩衝型 channel 的操作,則要 “寬鬆” 一些,畢竟是帶了 “緩衝” 光環。

對 chan 的發送和接收操作都會在編譯期間轉換成爲底層的發送接收函數。

Channel 分爲兩種:帶緩衝、不帶緩衝。對不帶緩衝的 channel 進行的操作實際上可以看作 “同步模式”,帶緩衝的則稱爲 “異步模式”。

同步模式下,發送方和接收方要同步就緒,只有在兩者都 ready 的情況下,數據才能在兩者間傳輸(後面會看到,實際上就是內存拷貝)。否則,任意一方先行進行發送或接收操作,都會被掛起,等待另一方的出現才能被喚醒。

異步模式下,在緩衝槽可用的情況下(有剩餘容量),發送和接收操作都可以順利進行。否則,操作的一方(如寫入)同樣會被掛起,直到出現相反操作(如接收)纔會被喚醒。

小結一下:同步模式下,必須要使發送方和接收方配對,操作纔會成功,否則會被阻塞;異步模式下,緩衝槽要有剩餘容量,操作纔會成功,否則也會被阻塞。

簡單來說,CSP 模型由併發執行的實體(線程或者進程或者協程)所組成,實體之間通過發送消息進行通信,
這裏發送消息時使用的就是通道,或者叫 channel。

CSP 模型的關鍵是關注 channel,而不關注發送消息的實體。Go 語言實現了 CSP 部分理論,goroutine 對應 CSP 中併發執行的實體,channel 也就對應着 CSP 中的 channel。

四、Goroutine

Goroutine 是實際併發執行的實體,它底層是使用協程(coroutine)實現併發,coroutine是一種運行在用戶態的用戶線程,類似於 greenthread,go底層選擇使用coroutine的出發點是因爲,它具有以下特點:

  • 用戶空間 避免了內核態和用戶態的切換導致的成本

  • 可以由語言和框架層進行調度

  • 更小的棧空間允許創建大量的實例

可以看到第二條 用戶空間線程的調度不是由操作系統來完成的,像在java 1.3中使用的greenthread的是由JVM統一調度的(後java已經改爲內核線程),還有在ruby中的fiber(半協程) 是需要在重新中自己進行調度的,而goroutine是在golang層面提供了調度器,並且對網絡IO庫進行了封裝,屏蔽了複雜的細節,對外提供統一的語法關鍵字支持,簡化了併發程序編寫的成本。

五、Goroutine 調度器

Go併發調度: G-P-M模型

在操作系統提供的內核線程之上,Go搭建了一個特有的兩級線程模型。goroutine機制實現了M : N的線程模型,goroutine機制是協程(coroutine)的一種實現,golang內置的調度器,可以讓多核CPU中每個CPU執行一個協程。

六、最後

Golang 的 channel 將 goroutine 隔離開,併發編程的時候可以將注意力放在 channel 上。在一定程度上,這個和消息隊列的解耦功能還是挺像的。如果大家感興趣,還是來看看 channel 的源碼吧,對於更深入地理解 channel 還是挺有用的。

Go 通過 channel 實現 CSP 通信模型,主要用於 goroutine 之間的消息傳遞和事件通知。

有了 channel 和 goroutine 之後,Go 的併發編程變得異常容易和安全,得以讓程序員把注意力留到業務上去,實現開發效率的提升。

要知道,技術並不是最重要的,它只是實現業務的工具。一門高效的開發語言讓你把節省下來的時間,留着去做更有意義的事情,比如寫寫文章。

CSP 最早是由 Tony Hoare在 1977 年提出,據說老爺子至今仍在更新這個理論模型,有興趣的朋友可以自行查閱電子版本:http://www.usingcsp.com/cspbook.pdf。

參考文檔:https://learnku.com/articles/32142Golang中國出品,文章對應源碼下載:https://www.qfgolang.com/?page_id=1973
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章