go語言并發(fā)原理和機制【二】

謝謝知乎不完善的文章編寫系統(tǒng)把我趕到了簡書:)

老規(guī)矩吧,廢話也懶得說了。知乎白瞎了我一篇文章,現(xiàn)在也不說多廢話。

緊接著上篇文章。


目錄


1.再探協(xié)程

什么是協(xié)程序,上一篇文章僅僅是一筆帶過,只說了他是一個比線程更加輕量級的東西;就好像一個函數(shù)。可是它到底是什么?它有沒有內(nèi)存自己的內(nèi)存組織?誰來調(diào)度它?

Coroutine的實現(xiàn),通常是對某個語言做相應的提議,然后通過后成編譯器標準,然后編譯器廠商來實現(xiàn)該機制。

Coroutine,翻譯成”協(xié)程",初始碰到的人馬上就會跟上面兩個概念聯(lián)系起來。

直接先說區(qū)別,Coroutine是編譯器級的,Process和Thread是 操作系統(tǒng)級的。Coroutine的實現(xiàn),通常是對某個語言做相應的提議,然后通過后成編譯器標準,然后編譯器廠商來實現(xiàn)該機制。

Process和Thread看起來也在語言層次,但是內(nèi)生原理卻是操作系統(tǒng)先有這個東西,然后通過一定的API暴露給用戶使用,兩者在這里有不同。Process和Thread是os通過調(diào)度算法,保存當前的上下文,然后從上次暫停的地方再次開始計算,重新開始的地方不可預期,每次CPU計算的指令數(shù)量和代碼跑過的CPU時間是相關的,跑到os分配的qpu時間到達后就會被os強制掛起。

Coroutine是編譯器的魔術,通過插入相關的代碼使得代碼段能夠?qū)崿F(xiàn)分段式的執(zhí)行,重新開始的地方是yield關鍵字指定的,- 次-定會跑到一-個yield對應的地方。

我們可以說:對于協(xié)程的調(diào)度,我們確實必須保存它的上下文,因為這是每個正常中斷再運行的函數(shù)所必須的;但這種操作,在go語言中,全權交給了語言本身;至于內(nèi)存的組織,應當是保存在所屬進程的棧中。

我們?yōu)槭裁葱枰獏f(xié)程?

(1)POSIX線程API是現(xiàn)有Unix進程模型的邏輯擴展,因此,線程可以獲得與進程相同的許多控件。線程有自己的信號掩碼,可以分配CPU資源,可以放在cgroups中,可以查詢它們使用哪些資源。所有這些控件都是Go程序使用goroutines所不需要的功能,這增加了開銷;當程序中有100,000個線程時,這些開銷就會迅速增加。

(2)另一個問題是,基于Go模型下,操作系統(tǒng)無法做出明智的調(diào)度決策。例如,Go垃圾收集器要求在運行收集時停止所有線程,并且內(nèi)存必須處于一致的狀態(tài)。這涉及到等待正在運行的線程到達一個我們知道的內(nèi)存一致的點。

發(fā)現(xiàn)了一張?zhí)貏e形象的動圖


2.CSP并發(fā)模型

(1)csp的提出

csp,communicating sequential processes,通信順序進程。1978年由C.A.R.Hoare首次在《Communications of the ACM》中提出。

本文認為輸入和輸出是程序設計的基本基元,通信順序過程的并行組合是程序設計的基本方法。當與Dijkstra的守衛(wèi)命令相結合時,這些概念是驚人的通用的。它們的用法可以通過各種熟悉的編程練習的示例解決方案來說明。

文中提到的Dijkstra的守衛(wèi)命令則來自另外一篇論文《Guarded Commands, Nondeterminacy and Formal Derivation? of Programs》。文中指出:

所謂的“保護命令”,是用來作為替代和重復構造的構建塊,這些替代和重復構造塊允許設置為不確定的程序組件;對于這些組件,至少所引發(fā)的活動(但也可能是最終狀態(tài))不一定由初始狀態(tài)確定。

說白了就是我們都知道一堆線程進行并發(fā)執(zhí)行,最終結果是不確定的;為了維護內(nèi)存或全局變量的一致完整性,我們加入“保護命令”,就好像互斥鎖。也有點像 if 判斷語句。

文中的語法是這樣的:

論文第二節(jié)截圖

簡而言之,一個右箭頭 → 就能概括全部:

對于:A → B;其中 A 是保護命令,是一個布爾類型的表達式;B是受保護命令的列表,大于等于一個。


說回Hoare的csp論文。

文中提到了幾點關于csp并發(fā)模型設計的原則,個人覺得很有意思:

(1)采用Dijkstra的保護命令(在符號上稍作改變)作為順序控制結構,并將其作為輸入和控制非決定語句的唯一手段;

(2)所有進程同時啟動,并行命令只有在它們?nèi)客瓿蓵r才結束。它們之間無法通過更新全局變量進行通信;

(3)輸入和輸出命令作為程序原語;它們用于并發(fā)進程之間的通信;

(4)當一個進程將另一個進程命名為輸出目的地,而第二個進程將第一個進程命名為輸入源時,就會發(fā)生這種通信;通常,一個輸入會等待輸入,這種等待往往是隨機的;

(5)輸入命令可能出現(xiàn)在保護中;

(6)輸入內(nèi)容可以進行模式匹配;

對于以上的基本原則,熟悉go語言并發(fā)協(xié)程之間通信方式的朋友一定都可以在go語言模式下找到對應的。比如對于第(4、6)點,可以看作是構造了一個通道var x chan int;對于第6點,這個通道只能輸入int類型;而對于第4點,可以看作是在項目中,一個協(xié)程輸入 x ← 2,另一個輸出 ← x,這是異步的,所以上文說“等待”。

理論上,Channel 和 Process 之間沒有從屬關系。Process 可以訂閱任意個 Channel,Process 沒必要擁有標識符,只有 Channel 需要,因為只向 Channel 發(fā)消息。不過具體實現(xiàn)也能把 Process 作為一個實體暴露出來。

當然要想深入學習csp模型,還是要跟著正規(guī)軍;大部分是圍繞著集合論等數(shù)學手段;

官方教程:http://usingcsp.com/cspbook.pdf

程序員視角審視csp,這里討論了JAVA實現(xiàn)CSP:https://www.cs.kent.ac.uk/projects/ofa/jcsp/cpa2007-jcsp.pdf

(2)csp的精髓

csp模型的精髓,一句話:不要用共享內(nèi)存的方式進行通信,要以通信方式共享內(nèi)存!

《七周七并發(fā)模型》中有特別形象的例子:

如果你和我一樣是個車迷,很可能只會關注車輛本身,而忽略了它所要行駛的道路。大家都在喋喋不休地爭論渦輪增壓與自然吸氣孰優(yōu)孰劣,讓中置發(fā)動機布局與前置發(fā)動機布局較高下,卻忘記了最重要的方面其實與車輛本身無關。你能去往何方、能多快到達目的地,首要的決定因素是道路網(wǎng)絡而不是車輛本身。

消息傳遞系統(tǒng)( message passing system)與之類似,決定其特性和功能的首要因素并不是用于傳遞消息的代碼或者消息的內(nèi)容,而是消息的傳輸通道。

這句話深刻的嵌入到go語言的設計當中。

一般線程同步在線程間交換的信息僅僅是控制信息,比如某個A線程釋放了鎖,B線程能獲取到鎖并開始運行,這個不涉及數(shù)據(jù)的交換。數(shù)據(jù)的交換主要還是通過共享內(nèi)存(共享變量或者隊列)來實現(xiàn),為了保證數(shù)據(jù)的安全和正確性,共享內(nèi)存就必需要加鎖等線程同步機制。而線程同步使用起來特別麻煩,容易造成死鎖,且過多的鎖會造成線程的阻塞以及這個過程中上下文切換帶來的額外開銷。

而在GO語言中,要傳遞某個數(shù)據(jù)給另一個goroutine(協(xié)程),可以把這個數(shù)據(jù)封裝成一個對象,然后把這個對象的指針傳入某個channel中,另外一個goroutine從這個channel中讀出這個指針,并處理其指向的內(nèi)存對象。

3.go并發(fā)模型

根據(jù)上篇文章,通常有3個線程模型。(1)一個是N:1,其中幾個用戶空間線程在一個OS線程上運行。這樣做的優(yōu)點是可以非??焖俚厍袚Q上下文,但是不能利用多核系統(tǒng);(2)另一個是1:1,其中一個執(zhí)行線程與一個操作系統(tǒng)線程匹配。它利用了機器上的所有核心,但是上下文切換很慢,因為它必須通過操作系統(tǒng)進行捕獲。

(1)架構

Go試圖通過使用M:N調(diào)度器來同時利用這兩個方面。它將任意數(shù)量的goroutine調(diào)度到任意數(shù)量的OS線程上。您可以快速切換上下文,并利用系統(tǒng)中的所有核心。這種方法的主要缺點是它增加了調(diào)度器的復雜性。為了完成調(diào)度任務,Go調(diào)度程序使用3個主要實體:

三角形表示一個OS線程。它是由操作系統(tǒng)管理的執(zhí)行線程,其工作方式與標準POSIX線程非常相似。在運行時代碼中,它被稱為M for machine。

圓圈代表goroutine。它包G括堆棧、指令指針和其他對調(diào)度goroutines很重要的信息,比如它可能阻塞的任何通道。在運行時代碼中,它被稱為G

矩形表示調(diào)度的上下文。您可以將它看作是在單個線程上運行Go代碼的調(diào)度程序的本地化版本。這是讓我們從N:1調(diào)度器過渡到M:N調(diào)度器的重要部分。在運行時代碼中,它被稱為P。

他們的組織結構如下:

上下文的數(shù)量在啟動時設置為GOMAXPROCS環(huán)境變量的值,或者通過運行時函數(shù)GOMAXPROCS()設置。通常情況下,這在程序執(zhí)行期間不會改變。上下文的數(shù)量是固定的,這意味著在任何時候都只有GOMAXPROCS在運行Go代碼。我們可以使用它來調(diào)優(yōu)Go進程對單個計算機的調(diào)用,比如在4核PC上運行Go代碼的4個線程。

灰色的goroutines沒有運行,但是已經(jīng)準備好了。它們被安排在名為runqueue的列表中。每當goroutine執(zhí)行go語句時,goroutine就被添加到運行隊列的末尾。一旦上下文運行了goroutine直到調(diào)度點,它就會從運行隊列中取出goroutine,設置堆棧和指令指針并開始運行goroutine。為了降低互斥爭用,每個上下文都有自己的本地運行隊列。


(2)調(diào)度

(a)阻塞

我們需要阻塞的一個例子是當我們調(diào)用一個系統(tǒng)調(diào)用。由于線程不能執(zhí)行在被阻塞在syscall上,所以我們需要傳遞上下文,以便它能夠繼續(xù)調(diào)度。

這里我們看到一個線程放棄了它的上下文,以便另一個線程可以運行它。調(diào)度程序確保有足夠的線程來運行所有上下文。上面例子中的M1可能只是為了處理這個syscall而創(chuàng)建的,也可能來自線程緩存。syscalling線程將保持使系統(tǒng)調(diào)用的goroutine,因為它在技術上仍然在執(zhí)行,盡管在操作系統(tǒng)中被阻塞了。

當syscall返回時,為了返回運行的goroutine,M0線程必須嘗試獲取一個上下文。

正常的操作模式是從其他線程竊取上下文;如果它不能竊取一個,它將把goroutine放到一個全局運行隊列中,把自己放到線程緩存中,然后休眠。全局運行隊列是上下文在其本地運行隊列用完時從中提取的運行隊列。上下文還會定期檢查全局運行隊列中的goroutines。否則,全球運行隊列上的goroutines可能會因為饑餓而停止運行。

(b)偷取G

當上下文耗盡了要調(diào)度的goroutines時。如果上下文的運行隊列上的工作量不平衡,就會發(fā)生這種情況。這可能導致上下文最終耗盡它的運行隊列,而系統(tǒng)中仍然有工作要做。要繼續(xù)運行Go代碼,上下文可以將goroutines從全局運行隊列中取出,但是如果其中沒有goroutines,它就必須從其他地方獲取它們。


某個地方是另一個環(huán)境。當一個上下文運行完時,它將嘗試從另一個上下文竊取大約一半的運行隊列。這確保在每個上下文中始終有工作要做,這又確保所有線程都在以最大容量工作。

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容