原著:翟陸續(加多) 資深Java , 著Java併發編程之美編程
本節咱們來探討Go的線程模型,首先咱們先來回顧下常見的三種線程模型,而後在介紹Go中獨特的線程模型。網絡
線程的併發執行是有操做系統來進行調度的,操做系統通常都都在內核提供對線程的支持。而咱們在使用高級語言編寫程序時候建立的線程是用戶線程,那麼用戶線程與內核線程是什麼關係那?其實下面將要講解的三種線程模型就是根據用戶線程與內核線程關係的不一樣而劃分的。多線程
這種線程模型下用戶線程與內核線程是一一對應的,當從程序入口點(好比main函數)啓動後,操做系統就建立了一個進程,這個main函數所在的線程就是主線程,在main函數內當咱們使用高級語言建立一個用戶線程的時候,其實對應建立了一個內核線程,以下圖:併發
這種線程模型優勢是在多處理器上,多個線程能夠真正實現並行運行,而且當一個線程因爲網絡IO等緣由被阻塞時候,其餘的線程不受影響。函數
缺點是因爲通常操做系統會限制內核線程的個數,因此用戶線程的個數會受到限制。另外因爲用戶線程與系統線程一一對應,當用戶線程好比執行Io操做(執行系統調用)時候,須要從用戶態的用戶程序的執行切換到內核態執行內核操做,而後等執行完畢後又會從內核態切換到用戶態執行用戶程序,而這個切換操做開銷是相對比較大的。操作系統
另外這裏提下高級語言Java的線程模型就是使用的這種一對一的模型,因此Java中多線程對共享變量使用鎖同步時候會致使獲取鎖失敗的線程進行上下文切換,而JUC包提供的無鎖CAS操做則不會產生上下文切換。線程
多對一模型是指多個用戶線程對應一個內核線程,同時同一個用戶線程只能對應一個內核線程,這時候對應同一個內核線程的多個用戶線程的上下文切換是由用戶態的運行時線程庫來作的,而不是由操做系統調度系統來作的,其模型以下:debug
這種模型好處是因爲上下文切換在用戶態,因此切換速度很快,開銷很小;另外可建立的用戶線程的數量能夠不少,只受內存大小限制。協程
這種模型因爲多個用戶線程對應一個內核線程,當該內核線程對應的一個用戶線程被阻塞掛起時候,該內核線程對應的其餘用戶線程也不能運行了,由於這時候內核線程已經被阻塞掛起了。另外這種模型並不能很好的利用多核CPU進行併發運行。blog
多對多模型則結合一對一和多對一模型的特色,讓大量的用戶線程對應少數幾個內核線程上,其模型圖以下:
這時候每一個內核線程對應多個用戶線程,每一個用戶線程有能夠對應多個內核線程,當一個用戶線程阻塞後,其對應的當前的內核線程會被阻塞,可是被阻塞的內核線程對應的其餘用戶線程能夠切換到其餘的內核線程上繼續運行,因此多對多模型是能夠充分利用多核CPU提高運行效能的。
另外多對多模型也對用戶線程個數沒有限制,理論上只要內存夠用能夠無限建立。
Go線程模型屬於多對多線程模型,其模型以下
Go中使用使用go語句建立的goroutine能夠認爲是輕量級的用戶線程,go線程模型包含三個概念:內核線程(M),goroutine(G),邏輯處理器(P),在Go中每一個邏輯處理器(P)會綁定到某一個內核線程上,每一個邏輯處理器(P)內有一個本地隊列,用來存放go運行時分配的goroutine。在上面介紹的多對多線程模型中是操做系統調度線程在物理CPU上運行,在Go中則是Go的運行時調度goroutine在邏輯處理器(P)上運行。
在go中存在兩級調度,一級是操做系統的調度系統,該調度系統調度邏輯處理器佔用cpu時間片運行,一級是go的運行時調度系統,該調度系統調度某個goroutine在邏輯處理上運行。
使用go語句建立一個goroutine後,建立的goroutine會被放入go運行時調度器的全局運行隊列中,而後go運行時調度器會把全局隊列中的goroutine分配給不一樣的邏輯處理器(P),分配的goroutine會被放到邏輯處理器(P)的本地隊列中,當本地隊列中某個goroutine就緒後待分配到時間片後就能夠在邏輯處理器上運行了,如上圖goroutine1當前正在佔用邏輯處理器1運行。
須要注意的是爲了不某些goroutine出現飢餓現象,被分配到某一個邏輯處理器(P)上的多個goroutine是分時在該邏輯處理器運行的,而不是獨佔運行直到結束,好比每一個goroutine從開始到運行結束須要10分鐘,那麼當前邏輯處理器下的goroutine1,goroutine2,goroutine3,並非順序執行,而是交叉併發運行的。
goroutine內部實現與在多個操做系統線程(Os 線程)之間複用的協程(coroutines)同樣。若是一個goroutine阻塞OS線程,例如等待輸入,則該OS線程對應的邏輯處理器P中的其餘goroutine將遷移到其餘OS線程,以便它們能夠繼續運行
如上圖左側假設goroutine1在執行文件文件讀取操做,則goroutine1會致使內核線程1阻塞,這時候go運行時調度器會把goroutine1所在的邏輯處理器1遷移到其餘的內核線程上(這裏是內核線程2上),這時候邏輯處理器1上的goroutine2和goroutine3就不會受goroutine1的影響了。等goroutine1文件讀取操做完成後goroutine1又會被go運行時調度系統從新放入到邏輯處理器1的本地隊列。
須要注意的是go運行時內核線程(M)的數量默認是10000個,你可使用runtime/debug包裏面的debug.SetMaxThreads(10000)來設置。
默認狀況下,Go默認是給每一個可用的物理處理器都分配一個邏輯處理器(p),若是你須要修改邏輯處理器(P)個數可使用runtime包的runtime.GOMAXPROCS函數設置.
至於goroutine(G)的數量則是由用戶程序本身來肯定,理論只要內存夠大,能夠無限制建立。
本節咱們探討了go的線程模型,講解了Go中是多對多的線程模型,正是因爲這種線程模型才讓go中每臺機器能夠建立成千上萬的goroutine(輕量級線程),瞭解了go的線程模型,特別是其中的MPG概念,就能夠隨業務須要動態設置最優方案。