? 輕量級進程模型:
用同步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