這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
輕量級進程模型:
用同步IO的方法寫程式的邏輯,第二點是用儘可能多的並發進程來提升IO並發的能力。
核心思想,第一:讓每個輕量級進程的資源佔用更小,建立進程個數的唯一限制便是記憶體大小。每個進程資源佔用越小的記憶體就能產生越高的並發性,記憶體資源是寶貴的,反而也是非常廉價的。第二:更輕量級的切換成本,把進程做到使用者態,這樣切換成本和函數的調用基本在同一個數量級,切換成本非常的低,如果是作業系統切換進程則需要從使用者態到核心態再到使用者態的切換。
輕量級進程的實現原理:
進程,進程本質上就是一個棧加上寄存器的狀態。進程切換,儲存當前的寄存器,讓後把寄存器修改為另外一個寄存器狀態,這樣就是等同於切換了棧,棧的位置其實也是寄存器維持的(esp/ebp)。
輕量級進程的底層還是線程池加上非同步IO,你可以把這個線程池中的每個線程想像成虛擬CPU(vCPU)。邏輯的輕量級進程routine的個數通常遠大於物理線程的,每個物理線程的同一時刻只有一個routine在跑,更多的routine是在等待當中。但是這個等待中的routine有兩種形式,一種是IO等待,還有一種是IO已完成的等待,或者是本身沒有任何前置條件,可以隨時參加調度。如果某一個物理的線程vcpu他的routine主動的或者是以為IO觸發了一個調度,把線程vcpu讓出來,這個時候就可以讓一個新的routine跑在上面,也就是從等待當中並且可以滿足調度的routine參與調度,按某種優先順序演算法選擇一個routine。所以輕量級進程調度原理是這樣的:
他是使用者態的線程,讓後有一個非搶佔式的調度機制,調度時機主要是由IO操作觸發的。發生IO操作的時候,IO操作的函數是這樣的:先發起一個非同步IO請求,發起後把這個routine狀態設定為等待IO完成,讓後讓出CPU,這是時候觸發調度器,調度器查看是否有任務等待調度,有就切換過去。然後再IO事件完成的時候,IO完成後會有個回掉函數作為IO完成的時間通知,這個會被調度器接管,回到函數把這個IO操作所屬的routine設定為Ready,可以參與就可以調度了。還有一個種是routine主動的讓出CPU,在這種情況下,routine的狀態在切換的時候還是Ready的,任何時候都可以切換到它。非搶佔操作的基礎的調度觸發條件:IO操作,IO完成時間,主動讓出CPU。使用者態的線程也可以實現搶佔式的調度,調度器起一個定時器,定時器發出一個定時任務,或者任務檢查每個正在執行當中的routine狀態,發現CPU佔用時間過長就讓他主動過的讓出CPU,這就是可以實現搶佔式的調度。
Go runtime的調度器:在瞭解Go的運行時的scheduler之前,需要先瞭解為什麼需要它,因為我們可能會想,OS核心不是已經有一個線程scheduler了嘛?
熟悉POSIX API的人都知道,POSIX的方案在很大程度上是對Unix process進場模型的一個邏輯描述和擴充,兩者有很多相似的地方。 Thread有自己的訊號掩碼,CPU affinity等。但是很多特徵對於Go程式來說都是累贅。 尤其是context環境切換的耗時。另一個原因是Go的記憶體回收需要所有的goroutine停止,使得記憶體在一個一致的狀態。記憶體回收的時間點是不確定的,如果依靠OS自身的scheduler來調度,那麼會有大量的線程需要停止工作。
單獨的開發一個GO得調度器,可以是其知道在什麼時候記憶體狀態是一致的,也就是說,當開始記憶體回收時,運行時只需要為當時正在CPU核上啟動並執行那個線程等待即可,而不是等待所有的線程。
使用者空間線程和核心空間線程之間的映射關係有:N:1,1:1和M:N
N:1是說,多個(N)使用者線程始終在一個核心線程上跑,context環境切換確實很快,但是無法真正的利用多核。
1:1是說,一個使用者線程就只在一個核心線程上跑,這時可以利用多核,但是上下文switch很慢。
M:N是說, 多個goroutine在多個核心線程上跑,這個看似可以集齊上面兩者的優勢,但是無疑增加了調度的難度。
Go的調度器內部有三個重要的結構:M,P,S
M:代表真正的核心OS線程,和POSIX裡的thread差不多,真正幹活的人
G:代表一個goroutine,它有自己的棧,instruction pointer和其他資訊(正在等待的channel等等),用於調度。
P:代表調度的上下文,可以把它看做一個局部的調度器,使go代碼在一個線程上跑,它是實現從N:1到N:M映射的關鍵。
圖中看,有2個物理線程M,每一個M都擁有一個context(P),每一個也都有一個正在啟動並執行goroutine。
P的數量可以通過GOMAXPROCS()來設定,它其實也就代表了真正的並發度,即有多少個goroutine可以同時運行。
圖中灰色的那些goroutine並沒有運行,而是出於ready的就緒態,正在等待被調度。P維護著這個隊列(稱之為runqueue),Go語言裡,啟動一個goroutine很容易:go function 就行,所以每有一個go語句被執行,runqueue隊列就在其末尾加入一個goroutine,在下一個調度點,就從runqueue中取出(如何決定取哪個goroutine?)一個goroutine執行。
為何要維護多個上下文P?因為當一個OS線程被阻塞時,P可以轉而投奔另一個OS線程!圖中看到,當一個OS線程M0陷入阻塞時,P轉而在OS線程M1上運行。調度器保證有足夠的線程來運行所以的context P。
圖中的M1可能是被建立,或者從線程緩衝中取出。當MO返回時,它必須嘗試取得一個context P來運行goroutine,一般情況下,它會從其他的OS線程那裡steal偷一個context過來,如果沒有偷到的話,它就把goroutine放在一個global runqueue裡,然後自己就去睡大覺了(放入線程緩衝裡)。Contexts們也會周期性的檢查global runqueue,否則global runqueue上的goroutine永遠無法執行。
另一種情況是P所分配的任務G很快就執行完了(分配不均),這就導致了一個上下文P閑著沒事兒幹而系統卻任然忙碌。但是如果global runqueue沒有任務G了,那麼P就不得不從其他的上下文P那裡拿一些G來執行。一般來說,如果上下文P從其他的上下文P那裡要偷一個任務的話,一般就‘偷’run queue的一半,這就確保了每個OS線程都能充分的使用。
ps:更多請參考 golang的goroutine是如何?的?
Erlang和golang的區別:
第一對鎖的態度不同,第二對非同步IO的態度不同,第三訊息機制不同。Erlang對鎖非常反感,認為變數不可變可以很大程度避免鎖。
Golang的觀點是鎖確實有很大的負擔,但是鎖基本上是無法避免的,一旦有人共用狀態並且互相搶佔去改變他,這時候鎖是必須存在的。
Erlang伺服器是單進程的,是邏輯上就沒有並發的東西,一個Process就是一個執行體,所以Erlang的伺服器和golang的伺服器不一樣,golang的伺服器是多進程的(goroutine)一起構成的一個伺服器。每個請求建立一個獨立的進程(goroutine)。但是Erlang不同,一個伺服器就是一個單進程的,所有的並發請求都進入到了進程郵箱,然後這個伺服器從進程郵箱裡取郵件(請求的內容)處理,Erlang的伺服器並沒有並發的請求,所以不需要所鎖。Erlang的高並發實現,第一:每個Erlang的物理進會有很多的伺服器,每個伺服器是互相無幹擾的,他們可以並發。第二是單伺服器高並發使用的是非同步IO。
go認為何時都不應該有非同步IO的代碼,Erlang則是在非同步IO的基礎上加上輕量級進程模型的混雜。
Golang對並發的支援,第一:價值迴歸,golang最重要的事情是讓執行成本降低,golang的棧最小可以到4K。第二:把執行體作為語言內建的標準設施(golang的代碼風格只有標準化得一種)。go得並行存取模型是最古老的並行存取模型,該並行存取模型包括,routine,原子操作,互斥體,同步,訊息,同步IO。
Ps:附帶講解ppt