天天看點

golang并發模型詳解

作者:幹飯人小羽
golang并發模型詳解

Golang排程器

  先看看golang排程的由來。

  一. 單程序時代不需要排程器

    在早期作業系統是單程序的,一個程序擁有整個系統的所有資源,是以也不需要排程器。

golang并發模型詳解

但是單程序的作業系統也有明顯的缺點:

   1. 采用單一的執行流程,計算機隻能一個任務一個任務處理。

   2. 程序阻塞所造成CPU資源的浪費。

那麼如何充分利用資源,可以讓多個程序同時并發的去執行呢?   是以後來作業系統就具有了最早的并發能力:多程序并發,當一個程序阻塞的時候,切換到另外等待執行的程序,這樣就能盡量把CPU利用起來,CPU就不浪費了。

 二. 多程序/線程作業系統

   在支援多程序和多線程的作業系統中,可以同時運作多個程序或者多個線程,CPU采用一定的政策,比如輪訓去執行多個程序或者線程,執行第一個程序然後在執行下一個程序,當一個程序阻塞CPU可以立刻切換到其他程序中去執行,而且排程CPU的政策可以保證在每個運作的程序/線程都可以被配置設定到CPU的運作時間片,這個時間片就是規定每個程序目前所運作的時間,當一個程序的時間片結束後,CPU就将目前程序上下文儲存再去執行下一個程序,該程序下一次運作隻能等到再次被配置設定到時間片在接着運作,因為每個時間片時間很短,這樣從宏觀的角度來看,似乎多個程序/線程是在同時被運作。

golang并發模型詳解

  程序/線程切換的缺點

  但是有新的問題,就是當某個執行程序擁有太多的資源,程序的建立、切換、銷毀,都會占用很長的時間,CPU雖然利用起來了,但如果程序過多,CPU有很大的一部分都被用來進行程序排程了,比如程序的上下文切換,就要儲存目前程序的資訊,這就需要系統調用,以及拷貝/複制等這些操作,是以程序/線程數量越多,切換的成本就越大(尤其是程序),CPU要處理切換這些事情,造成對CPU的使用率就越低,是以對性能就越有影響。   

golang并發模型詳解
golang并發模型詳解

  程序比較重,占用資源比較大,比較占用記憶體空間,CPU切換必然對性能有很大的影響。

  相對于程序,線程雖然比程序輕量,也被稱為輕量級程序(Lightweight Process,LWP),但是實際上多線程開發設計會變得更加複雜,要考慮很多同步競争等問題,如鎖、競争沖突等,開發也變得非常複雜。

那麼怎麼才能提高CPU的使用率呢?

三. 使用協程來提高CPU使用率   協程(coroutine)被稱為是一種使用者态的輕量級線程,協程也是一種線程,而一個線程分為"核心态"的線程和"使用者态"的線程,一個“使用者态線程”必須要綁定一個“核心态線程”,但是CPU并不知道有“使用者态線程”的存在,CPU隻知道它運作的是一個“核心态線程”(Linux的PCB程序控制塊),如下圖所示。   

golang并發模型詳解

  在Go語言中,協程(coroutine)是Go語言中的輕量級線程實作,那麼我們可以核心線程依然叫 “線程 (thread)”,而使用者線程叫 “協程 (co-routine)”,如下。

golang并發模型詳解

上圖裡一個協程 (co-routine) 可以綁定一個線程 (thread),這種是一個協程對應一個線程,是以這種是1:1模型。協程的建立,删除,切換也都是需要CPU去完成的。

當然多個協程 (co-routine) 也可以綁定一個或者多個線程 (thread),這樣就是N個協程對應一個線程 。

golang并發模型詳解

上圖三個協程在使用者态,對應核心空間的一個線程,使用者态重的協程在工作時候也是可以進行切換的,但是這種切換是在使用者态,協程之間的切換是由圖中協程排程器去完成的,這樣帶來的好處就是協程在使用者态線程即完成切換,不會陷入到核心态,這種切換非常的輕量快速。

  但是N:1這種模型也是有缺點的

  N:1模型的缺點:

  • 某個程式用不了硬體的多核加速能力。
  • 一旦某協程阻塞,造成線程阻塞,本程序的其他協程都無法執行了,根本就沒有并發的能力了。

進一步改良M:N模型,就是M個協程對應N個線程,如下圖所示。

golang并發模型詳解

上圖核心态有兩個線程,當有兩個CPU的時候,就可以利用多核,每個CPU可以綁定一個線程,不過這種處理模式就更加的複雜了,線程和協程之間都是通過協程排程器去協作和管理,是以排程器的性能就顯得非常重要。

四. Go語言中的協程goroutine

  在Go語言中,協程被稱為goroutine,它非常輕量,一個goroutine隻占幾KB,這個比線程輕量一個數量級,因為占用記憶體小,是以排程更靈活 (runtime 排程),切換也可以很頻繁。無論協程還是線程都需要排程器去完成排程,我們需要了解最關鍵的排程協程的排程器的實作原理。

  來看看早期Go語言的goroutine排程器如何實作的。

golang并發模型詳解

  在早期goroutine排程器中,每建立一個協程goroutine就會被添加到一個全局go協程隊列中,當線程M0想要擷取一個goroutine時候,就去從全局go協程隊列中擷取,全局go協程隊列會有一個鎖來保護,當線程擷取鎖之後就從全局隊列中拿到一個goroutine并去執行,執行後就去将鎖換回去,并将goroutine放回,這就是一個完整的過程。

  這種排程器比較簡單,但是也是有缺點的

  缺點:

    1. 建立,銷毀,排程goroutine需要每個線程都先獲得鎖,這樣就容易形成鎖競争。

    2. 線程轉移goroutine會造成延遲和額外的系統負載。比如線程(M1)當執行中的goroutine(G1)中包含建立新協程的時候,線程(M1)建立新的一個goroutine(G2),為了繼續執行這個新的goroutine(G2),需要把這個新的goroutine(G2)交給另一個線程(M2)執行,也造成了很差的局部性,因為 (G2)和(G1)是相關的,最好放在(M1)上執行,而不是其他(M2)。

    3. 系統調用 (CPU 在 M 之間的切換) 導緻頻繁的線程阻塞和取消阻塞操作增加了系統開銷。

CSP模型介紹

  CSP模型的全稱是Communicating Sequential Process,翻譯是通信順序程序,是一種并發程式設計模型,還有一種并發模型是Actor模式,著名的并發程式設計語言Erlang就是用的Actor模式。csp模型在上個世紀70年代就提出來了,是用于描述兩個獨立的并發實體通過共享的通訊channel(也就是管道)來進行通信的并發模型。相對于Actor模型來說,CSP中的channel是第一類對象,它不關注發送消息的實體,而關注與發送消息時候所使用的channel,CSP模型是一種很強大的并發通訊模型,也成為面向并發程式設計語言的理論源頭,也就誕生出後來的golang等語言。

  對于golang來說,其實隻用到了CSP模型的很小一部分,即Process/Channel,對應golang語言就是goroutine/channel,這兩個并發關鍵字之間沒有從屬關系,Process可以是一個程序,線程,甚至可以是一個代碼塊,Process可以訂閱任意個Channel,Channel也可不關心是從那個Process在利用它通信,Process圍繞Channel進行讀寫。

  goroutine 是通過 GMP 排程模型實作的。

  

  G: 表示一個 goroutine,它有自己的棧。

M: 表示核心級線程,一個 M 就是一個線程,goroutine 跑在 M 之上的。

  P: 全稱是 Processor,處理器。它主要用來執行 goroutine 的,同時它也維護了一個 goroutine 隊列。