爲什麼Golang需要單獨開發一個Goroutine?
開銷問題:
POSIX的thread API雖然能夠提供豐富的API,例如配置自己的CPU親和性,申請資源等等,線程在得到了很多與進程相同的控制權的同時,開銷也非常的大,在Goroutine中則不需這些額外的開銷,所以一個Golang的程序中可以支持10w級別的Goroutine。
調度性能:
在Golang的程序中,操作系統級別的線程調度,通常不會做出合適的調度決策。例如在GC時,內存必須要達到一個一致的狀態。在Goroutine機制裏,Golang可以控制Goroutine的調度,從而在一個合適的時間進行GC。
Goroutine的實現原理
- 兩種備選方案
- (M:1)多個用戶態的線程對應一個系統線程,它可以做快速的上下文切換。缺點是不能有效利用多核CPU
- (1:1)一個用戶態的線程對應一個系統線程,它可以利用多核機制,但上下文切換需要消耗額外的資源
Golang的做法
- (M:N)Golang採取了一種多對多的方案。M個用戶線程對應N個系統線程,缺點增加了調度器的實現難度
角色:
- M: 代表了系統線程,由操作系統管理
- G:Goroutine的實體,包括了調用棧,重要的調度信息,例如channel等。
P:銜接M和G的調度上下文,它負責將等待執行的G與M對接。
P的數量由環境變量中的
GOMAXPROCS
決定,通常來說它是和核心數對應,例如在4Core的服務器上回啓動4個線程。G會有很多個,每個P會將Goroutine從一個就緒的隊列中做Pop操作,爲了減小鎖的競爭,通常情況下每個P會負責一個隊列。
掛起
在Goroutine需要執行一個系統調用時,由於M是一個線程,所以必須等待它執行完才能執行其他的Goroutine。當一個新的Goroutine產生,M需要保證會有另外的一個M能夠執行這個G,簡單來說,當一個M進行系統調用,需要保證有另外的一個M能夠繼續執行Go代碼。
何時恢復?
當系統調用返回時,M需要找到一個對應的P,以便能夠運行Goroutine,它首先會嘗試從其他線程中竊取一個P,如果不成功,它會將Goroutine放在一個全局的隊列中,並將自己放在thread cache中。
如何竊取
這裏有篇paper來描述這個設計:work-steal.
簡單來說,當隊列不平衡時,會從其他隊列中截取一部分Goroutine到P上進行調度。
參考鏈接
https://docs.google.com/document/d/1TTj4T2JO42uD5ID9e89oa0sLKhJYD0Y_kqxDv3I3XMw/edit#
https://morsmachine.dk/go-scheduler
(翻譯)[https://www.zhihu.com/question/20862617]