Golang 協程

elihe2011 發表於 2020-10-17
Go

1. CSP 併發模型

Communication Sequential Processes

Do not communicate by sharing memory; instead, share memory by communicating.

不要通過共享記憶體來通訊,而要通過通訊實現記憶體共享

Go的CSP併發模型,通過goroutine和channel來實現:

  • goroutine: 併發的執行單位

    Goroutine是Golang併發的實體,它底層使用協程實現併發,coroutine是一種執行在使用者態的使用者執行緒,類似greenthread,具有如下特點:

    • 使用者空間,避免了核心態和使用者態的切換導致的成本
    • 可由語言和框架層進行排程
    • 更小的棧空間允許建立大量的例項
  • channel: 併發的通訊機制

    channel被單獨建立,可以在程式之間傳遞,一個實體通過將訊息發到channel中,然後又監聽這個channel的實體處理,兩個實體之間是匿名的,它實現了實體中間的解藕。

2. 協程、執行緒、程式

  • 程式:系統進行資源分配和排程的一個獨立單位。每個程式都有自己獨立的記憶體空間,不同程式通過IPC通訊。程式上下文切換開銷(棧、暫存器、虛擬記憶體、檔案控制程式碼)比較大,但相對比較穩定安全。

  • 執行緒:處理器排程和分配的基本單位。執行緒是程式內部的一個執行單元,每個程式至少有一個主執行緒,它無需使用者去主動建立,由系統自動建立。執行緒間通訊主要通過共享記憶體、上下文切換較快,資源開銷小,但相比程式不夠穩定,容易丟資料

  • 協程:使用者態輕量級執行緒,它的排程完全由使用者控制。協程擁有自己的暫存器上下文和棧。協程排程切換時,將暫存器上下文和棧儲存到其他地方,在切回來的時候,恢復先前儲存的暫存器上下文和棧。開銷小,切換快。

程式、執行緒、協程的關係和區別:

  • 程式:擁有獨立的堆和棧,由作業系統排程
  • 執行緒:擁有獨立的棧,但共享堆。由作業系統排程(標準執行緒)
  • 協程:和執行緒一樣,擁有獨立的棧,共享堆。由程式開發者在程式碼中顯示排程

為什麼協程比執行緒輕量?

  1. 記憶體消耗極小:
    • goroutine: 2KB, 如果棧記憶體不足,自動擴容
    • thread: 1M, 另外還需要一個"a guard page"區域用於與其他執行緒的棧空間進行隔離
  2. 建立和銷燬損耗資源小:
    • goroutine: 由Go runtime負責管理,建立和銷燬的消耗非常小,是使用者級別的。
    • thread: 是核心級的,建立和銷燬都會有巨大的消耗,一般通過執行緒池來緩解
  3. 切換快:
    • goroutine: 不依賴於系統,由golang自己實現的CSP併發模型實現:G-P-M。go協程也叫使用者態執行緒,協程的切換髮生在使用者態,約為200ns
    • thread: 核心對外提供的服務,應用程式可以通過系統呼叫讓核心啟動執行緒。執行緒在等待IO操作時變成unrunnable狀態觸發上下文切換,1000~1500ns

3. 併發的實現原理

KSE:Kernel Scheduling Entity, 核心排程實體,即可以被作業系統核心排程器排程的實體物件,它是核心的最小排程單元,也就是核心級執行緒

三種執行緒模型:

  • 使用者級執行緒模型
  • 核心級執行緒模型
  • 兩級執行緒模型(即混合型執行緒模型)

3.1 使用者級執行緒模型

user

多個使用者態的執行緒對應一個核心執行緒,執行緒的建立、終止、切換或同步等工作必須自身來完成;

優點:執行緒排程在使用者層面完成,不存在CPU在使用者態和核心態之間切換,輕量級,對系統資源消耗少

缺點:做不到真正意義上的併發。如果存在某個執行緒阻塞呼叫(比如I/O操作),其他執行緒將被阻塞,整個程式被掛起。因為在使用者執行緒模式下,程式內的執行緒繫結到CPU是由使用者程式排程實現的,內部執行緒對CPU不可見,即CPU排程的是程式,而非執行緒

Python協程庫gevent,把阻塞的操作重新封裝為完成給阻塞模式,在阻塞點上,主動讓出自己,並通知或喚醒其他等待的使用者執行緒。

3.2 核心級執行緒模型

kernel

直接呼叫作業系統的核心執行緒,所有執行緒的建立、終止、切換、同步等操作,都由核心完成

優點:簡單。直接藉助核心的執行緒和排程器,可以快速實現執行緒切換,做到真正的併發處理

缺點:直接使用核心區建立、銷燬及執行緒上下文切換和排程,系統資源開銷大,影響效能

Java/C++ 執行緒庫

3.3 兩級執行緒模型(即混合型執行緒模型)

mixed

一個程式可與多個核心執行緒KSE關聯,該程式內的多個執行緒繫結到了不同的KSE上

程式內的執行緒並不與KSE一一繫結,當某個KSE繫結的執行緒因阻塞操作被核心排程出CPU時,其關聯的程式中的某個執行緒又會重新與KSE繫結

為什麼稱為兩級?使用者排程實現使用者執行緒到KSE的排程,核心排程器實現KSE到CPU上的排程

Golang

4. G-P-M 模型

gpm

G-P-M 模型:

  • G:Goroutine:獨立執行單元。相較於每個OS執行緒固定分配2M記憶體的模式,Goroutine的棧採取動態擴容方式,2k ~ 1G(AMD64, AMD32: 256M)。週期性回收記憶體,收縮棧空間
    • 每個Goroutine對應一個G結構體,它儲存Goroutine的執行堆疊、狀態及任務函式,可重用。
    • G並非執行體,每個G需要繫結到P才能被排程執行
  • P:Processor: 邏輯處理器,中介
    • 對G來說,P相當於CPU,G只有繫結到P才能被呼叫
    • 對M來說,P提供相關的執行環境(Context),如記憶體分配狀態(mcache),任務佇列(G)等
    • P的數量決定系統最大並行的G的數量 (CPU核數 >= P的數量),使用者可通過GOMAXPROCS設定數量,但不能超過256
  • M:Machine
    • OS執行緒抽象,真正執行計算的資源,在繫結有效的P後,進入schedule迴圈
    • schedule迴圈的機制大致從Global佇列、P的Local佇列及wait佇列中獲取G,切換到G的執行棧上執行G的函式,呼叫goexit做清理工作並回到M
    • M不保留G的狀態
    • M的數量不定,由Go Runtime調整,目前預設不超過10K

5. Golang 併發控制模型

  • channel
  • sync.WaitGroup: Add(), Done(), Wait()
  • Context: Done(), Err(), Deadline(), Value() 較複雜的併發

6. gorountine排程時機

情形說明
gogo 建立一個新的 goroutine,Go scheduler 會考慮排程
GC由於進行 GC 的 goroutine 也需要在 M 上執行,因此肯定會發生排程。當然,Go scheduler 還會做很多其他的排程,例如排程不涉及堆訪問的 goroutine 來執行。GC 不管棧上的記憶體,只會回收堆上的記憶體
系統呼叫當 goroutine 進行系統呼叫時,會阻塞 M,所以它會被排程走,同時一個新的 goroutine 會被排程上來
記憶體同步訪問atomic,mutex,channel 操作等會使 goroutine 阻塞,因此會被排程走。等條件滿足後(例如其他 goroutine 解鎖了)還會被排程上來繼續執行