每個OS線程都有一個固定大小的內存塊(通常會是2MB)來作棧,這個棧會用來存儲當前正在被調用或掛起(指在調用其它函數時)的函數的內部變量。這個固定大小的棧同時很大又很小。由於2MB的棧對於一個小小的goroutine來講是很大的內存浪費,而對於一些複雜的任務(如深度嵌套的遞歸)來講又顯得過小。所以,Go語言作了它本身的『線程』。golang
在Go語言中,每個goroutine是一個獨立的執行單元,相較於每一個OS線程固定分配2M內存的模式,goroutine的棧採起了動態擴容方式, 初始時僅爲2KB,隨着任務執行按需增加,最大可達1GB(64位機器最大是1G,32位機器最大是256M),且徹底由golang本身的調度器 Go Scheduler 來調度。此外,GC還會週期性地將再也不使用的內存回收,收縮棧空間。 所以,Go程序能夠同時併發成千上萬個goroutine是得益於它強勁的調度器和高效的內存模型。Go的創造者大概對goroutine的定位就是屠龍刀,由於他們不只讓goroutine做爲golang併發編程的最核心組件(開發者的程序都是基於goroutine運行的)並且golang中的許多標準庫的實現也處處能見到goroutine的身影,好比net/http這個包,甚至語言自己的組件runtime運行時和GC垃圾回收器都是運行在goroutine上的,做者對goroutine的厚望可見一斑。web
任何用戶線程最終確定都是要交由OS線程來執行的,goroutine(稱爲G)也不例外,可是G並不直接綁定OS線程運行,而是由Goroutine Scheduler中的 P - Logical Processor (邏輯處理器)來做爲二者的『中介』,P能夠看做是一個抽象的資源或者一個上下文,一個P綁定一個OS線程,在golang的實現裏把OS線程抽象成一個數據結構:M,G其實是由M經過P來進行調度運行的,可是在G的層面來看,P提供了G運行所需的一切資源和環境,所以在G看來P就是運行它的 「CPU」,由 G、P、M 這三種由Go抽象出來的實現,最終造成了Go調度器的基本結構:算法
關於P,咱們須要再絮叨幾句,在Go 1.0發佈的時候,它的調度器其實G-M模型,也就是沒有P的,調度過程全由G和M完成,這個模型暴露出一些問題:編程
這些問題實在太扎眼了,致使Go1.0雖然號稱原生支持併發,卻在併發性能上一直飽受詬病,而後,Go語言委員會中一個核心開發大佬看不下了,親自下場從新設計和實現了Go調度器(在原有的G-M模型中引入了P)而且實現了一個叫作 work-stealing 的調度算法:緩存
該算法避免了在goroutine調度時使用全局鎖。網絡
至此,Go調度器的基本模型確立:數據結構
Go調度器工做時會維護兩種用來保存G的任務隊列:一種是一個Global任務隊列,一種是每一個P維護的Local任務隊列。併發
當經過go
關鍵字建立一個新的goroutine的時候,它會優先被放入P的本地隊列。爲了運行goroutine,M須要持有(綁定)一個P,接着M會啓動一個OS線程,循環從P的本地隊列裏取出一個goroutine並執行。固然還有上文說起的 work-stealing
調度算法:當M執行完了當前P的Local隊列裏的全部G後,P也不會就這麼在那躺屍啥都不幹,它會先嚐試從Global隊列尋找G來執行,若是Global隊列爲空,它會隨機挑選另一個P,從它的隊列裏中拿走一半的G到本身的隊列中執行。函數
若是一切正常,調度器會以上述的那種方式順暢地運行,但這個世界沒這麼美好,總有意外發生,如下分析goroutine在兩種例外狀況下的行爲。性能
Go runtime會在下面的goroutine被阻塞的狀況下運行另一個goroutine:
這四種場景又可歸類爲兩種類型:
當goroutine由於channel操做或者network I/O而阻塞時(實際上golang已經用netpoller實現了goroutine網絡I/O阻塞不會致使M被阻塞,僅阻塞G,這裏僅僅是舉個栗子),對應的G會被放置到某個wait隊列(如channel的waitq),該G的狀態由_Gruning
變爲_Gwaitting
,而M會跳過該G嘗試獲取並執行下一個G,若是此時沒有runnable的G供M運行,那麼M將解綁P,並進入sleep狀態;當阻塞的G被另外一端的G2喚醒時(好比channel的可讀/寫通知),G被標記爲runnable,嘗試加入G2所在P的runnext,而後再是P的Local隊列和Global隊列。
當G被阻塞在某個系統調用上時,此時G會阻塞在_Gsyscall
狀態,M也處於 block on syscall 狀態,此時的M可被搶佔調度:執行該G的M會與P解綁,而P則嘗試與其它idle的M綁定,繼續執行其它G。若是沒有其它idle的M,但P的Local隊列中仍然有G須要執行,則建立一個新的M;當系統調用完成後,G會從新嘗試獲取一個idle的P進入它的Local隊列恢復執行,若是沒有idle的P,G會被標記爲runnable加入到Global隊列。
以上就是從宏觀的角度對Goroutine和它的調度器進行的一些概要性的介紹,固然,Go的調度中更復雜的搶佔式調度、阻塞調度的更多細節,你們能夠自行去找相關資料深刻理解,本文只講到Go調度器的基本調度過程,爲後面本身實現一個Goroutine Pool提供理論基礎,這裏便再也不繼續深刻上述說的那幾個調度了,事實上若是要徹底講清楚Go調度器,一篇文章的篇幅也實在是捉襟見肘,因此想了解更多細節的同窗能夠去看看Go調度器 G-P-M 模型的設計者 Dmitry Vyukov 寫的該模型的設計文檔《 Go Preemptive Scheduler Design》以及直接去看源碼,G-P-M模型的定義放在src/runtime/runtime2.go
裏面,而調度過程則放在了src/runtime/proc.go
裏。
REFERENCE: