Golang 協程的理論知識部分

2020-08-12 16:28:22

作爲一門 21 世紀的語言,Go 原生支援應用之間的通訊(網路,用戶端和伺服器端,分佈式計算,參見第 15 章)和程式的併發。程式可以在不同的處理器和計算機上同時執行不同的程式碼段。Go 語言爲構建併發程式的基本程式碼塊是 協程 (goroutine) 與通道 (channel)。他們需要語言,編譯器,和 runtime 的支援。Go 語言提供的垃圾回收器對併發程式設計至關重要。

不要通過共用記憶體來通訊,而通過通訊來共用記憶體。

通訊強制共同作業。

————————————————

 

爲什麼用協程不用執行緒?

 

並行是一種通過使用多處理器以提高速度的能力。所以併發程式可以是並行的,也可以不是。

公認的,使用多執行緒的應用難以做到準確,最主要的問題是記憶體中的數據共用,它們會被多執行緒以無法預知的方式進行操作,導致一些無法重現或者隨機的結果(稱作 競態)。

不要使用全域性變數或者共用記憶體,它們會給你的程式碼在併發運算的時候帶來危險。

解決之道在於同步不同的執行緒,對數據加鎖,這樣同時就只有一個執行緒可以變更數據。在 Go 的標準庫 sync 中有一些工具用來在低級別的程式碼中實現加鎖;我們在第 9.3 節中討論過這個問題。不過過去的軟件開發經驗告訴我們這會帶來更高的複雜度,更容易使程式碼出錯以及更低的效能,所以這個經典的方法明顯不再適合現代多核 / 多處理器程式設計:thread-per-connection 模型不夠有效。

Go 更傾向於其他的方式,在諸多比較合適的範式中,有個被稱作 Communicating Sequential Processes(順序通訊處理)(CSP, C. Hoare 發明的)還有一個叫做 message passing-model(訊息傳遞)(已經運用在了其他語言中,比如 Erlang)。

 

協程是輕量的,比執行緒更輕。它們痕跡非常不明顯(使用少量的記憶體和資源):使用 4K 的棧記憶體就可以在堆中建立它們。因爲建立非常廉價,必要的時候可以輕鬆建立並執行大量的協程(在同一個地址空間中 100,000 個連續的協程)。並且它們對棧進行了分割,從而動態的增加(或縮減)記憶體的使用;棧的管理是自動的,但不是由垃圾回收器管理的,而是在協程退出後自動釋放。

 

通過通訊共用記憶體
併發程式設計是個很大的論題。但限於篇幅,這裏僅討論一些 Go 特有的東西。

在併發程式設計中,爲實現對共用變數的正確存取需要精確的控制,這在多數環境下都很困難。 Go 語言另闢蹊徑,它將共用的值通過通道傳遞,實際上,多個獨立執行的執行緒從不會主動共用。 在任意給定的時間點,只有一個 Go 協程能夠存取該值。數據競爭從設計上就被杜絕了。 爲了提倡這種思考方式,我們將它簡化爲一句口號:

不要通過共用記憶體來通訊,而應通過通訊來共用記憶體。

這種方法意義深遠。例如,參照計數通過爲整數變數新增互斥鎖來很好地實現。 但作爲一種高階方法,通過通道來控制存取能夠讓你寫出更簡潔,正確的程式。

我們可以從典型的單執行緒執行在單 CPU 之上的情形來審視這種模型。它無需提供同步原語。 現在考慮另一種情況,它也無需同步。現在讓它們倆進行通訊。若將通訊過程看做同步着, 那就完全不需要其它同步了。例如,Unix 管道就與這種模型完美契合。 儘管 Go 的併發處理方式來源於 Hoare 的通訊順序處理(CSP), 它依然可以看做是型別安全的 Unix 管道的實現。

協程(goroutine)
我們稱之爲 Go 協程是因爲現有的術語 — 執行緒、協程、進程等等 — 無法準確傳達它的含義。 Go 協程具有簡單的模型:它是與其它 Go 協程併發執行在同一地址空間的函數。它是輕量級的, 所有消耗幾乎就只有棧空間的分配。而且棧最開始是非常小的,所以它們很廉價, 僅在需要時纔會隨着堆空間的分配(和釋放)而變化。

Go 協程在多執行緒操作系統上可實現多路複用,因此若一個執行緒阻塞,比如說等待 I/O, 那麼其它的執行緒就會執行。Go 協程的設計隱藏了執行緒建立和管理的諸多複雜性。

在函數或方法前新增 go 關鍵字能夠在新的 Go 協程中呼叫它。當呼叫完成後, 該 Go 協程也會安靜地退出。(效果有點像 Unix Shell 中的 & 符號,它能讓命令在後台執行。)