天天看點

Go原生支援的并發方案goroutine

作者:jodkreaper

Go 的設計者敏銳地把握了 CPU 向多核方向發展的這一趨勢,在決定去建立 Go 語言的時候,他們果斷将面向多核、原生支援并發作為了 Go 語言的設計目标之一,并将面向并發作為 Go 的設計哲學。

這裡要區分并發和并行是兩個概念,并發不是并行,并發關乎結構,并行關乎執行,采用并發設計的程式并不一定是并行執行的。在多核處理器成為主流的時代,即使采用并發設計的應用程式以單執行個體的方式運作,其中的每個内部子產品也都是運作于一個單獨的線程中的,多核資源也可以得到充分利用。而且,并發讓并行變得更加容易,采用并發設計的應用可以将負載自然擴充到各個 CPU 核上,進而提升處理器的利用效率。

Go 的并發方案:goroutine

Go 并沒有使用作業系統線程作為承載分解後的代碼片段(子產品)的基本執行單元,而是實作了goroutine這一由 Go 運作時(runtime)負責排程的、輕量的使用者級線程,為并發程式設計提供原生支援。

goroutine 的優勢:

1、資源占用小,每個 goroutine 的初始棧大小僅為 2k;

2、由 Go 運作時而不是作業系統排程,goroutine 上下文切換在使用者層完成,開銷更小;

3、在語言層面而不是通過标準庫提供。goroutine 由go關鍵字建立,一退出就會被回收或銷毀,開發體驗更佳;

4、語言内置 channel 作為 goroutine 間通信原語,為并發設計提供了強大支撐。

接下來,我們來看看在 Go 中究竟如何使用 goroutine。

goroutine 的基本用法

并發是一種能力,它讓你的程式可以由若幹個代碼片段組合而成,并且每個片段都是獨立運作的。goroutine 恰恰就是 Go 原生支援并發的一個具體實作。無論是 Go 自身運作時代碼還是使用者層 Go 代碼,都無一例外地運作在 goroutine 中。

建立goroutine:

Go 語言通過go關鍵字+函數/方法的方式建立一個 goroutine。建立後,新 goroutine 将擁有獨立的代碼執行流,并與建立它的 goroutine 一起被 Go 運作時排程。

go fmt.Println("I am a goroutine")

var c = make(chan int)
go func(a, b int) {
    c <- a + b
}(3,4)
 
// $GOROOT/src/net/http/server.go
c := srv.newConn(rw)
go c.serve(connCtx)           

我們看到,通過 go 關鍵字,我們可以基于已有的具名函數 / 方法建立 goroutine,也可以基于匿名函數 / 閉包建立 goroutine。

建立 goroutine 後,go 關鍵字不會傳回 goroutine id 之類的唯一辨別 goroutine 的 id,你也不要嘗試去得到這樣的 id 并依賴它。另外,和線程一樣,一個應用内部啟動的所有 goroutine 共享程序空間的資源,如果多個 goroutine 通路同一塊記憶體資料,将會存在競争,我們需要進行 goroutine 間的同步。

退出goroutine:

goroutine 的使用代價很低,Go 官方也推薦你多多使用 goroutine。而且,多數情況下,我們不需要考慮對 goroutine 的退出進行控制:goroutine 的執行函數的傳回,就意味着 goroutine 退出。要注意的是,goroutine 執行的函數或方法即便有傳回值,Go 也會忽略這些傳回值。是以,如果你要擷取 goroutine 執行後的傳回值,你需要另行考慮其他方法,比如通過 goroutine 間的通信來實作。

goroutine 間的通信

傳統的程式設計語言(比如:C++、Java、Python 等)并非面向并發而生的,是以他們面對并發的邏輯多是基于作業系統的線程。并發的執行單元(線程)之間的通信,利用的也是作業系統提供的線程或程序間通信的原語,比如:共享記憶體、信号(signal)、管道(pipe)、消息隊列、套接字(socket)等。

可以看出傳統語言的并發模型是基于對記憶體的共享,這樣要花費大量心思設計線程間的同步機制,并且在設計同步機制的時候,還要考慮多線程間複雜的記憶體管理,以及如何防止死鎖等情況。

但 Go 語言就不一樣了!Go 語言從設計伊始,就将解決上面這個傳統并發模型的問題作為 Go 的一個目标,并在新并發模型設計中借鑒了著名計算機科學家Tony Hoare提出的 CSP(Communicating Sequential Processes,通信順序程序)并發模型。

Tony Hoare 的 CSP 理論中的 P,也就是“Process(程序)”,是一個抽象概念,它代表任何順序處理邏輯的封裝,它擷取輸入資料(或從其他 P 的輸出擷取),并生産出可以被其他 P 消費的輸出資料。這裡我們可以簡單看下 CSP 通信模型的示意圖:

Go原生支援的并發方案goroutine

注意了,這裡的 P 并不一定與作業系統的程序或線程劃等号。在 Go 中,與“Process”對應的是 goroutine。為了實作 CSP 并發模型中的輸入和輸出原語,Go 還引入了 goroutine(P)之間的通信原語channel。goroutine 可以從 channel 擷取輸入資料,再将處理後得到的結果資料通過 channel 輸出。通過 channel 将 goroutine(P)組合連接配接在一起。

比如我們上面提到的擷取 goroutine 的退出狀态,就可以使用 channel 原語實作:

func spawn(f func() error) <-chan error {
    c := make(chan error)

    go func() {
        c <- f()
    }()

    return c
}

func main() {
    c := spawn(func() error {
        time.Sleep(2 * time.Second)
        return errors.New("timeout")
    })
    fmt.Println(<-c)
}           

這個示例在 main goroutine 與子 goroutine 之間建立了一個元素類型為 error 的 channel,子 goroutine 退出時,會将它執行的函數的錯誤傳回值寫入這個 channel,main goroutine 可以通過讀取 channel 的值來擷取子 goroutine 的退出狀态。

注意:雖然 CSP 模型已經成為 Go 語言支援的主流并發模型,但 Go 也支援傳統的、基于共享記憶體的并發模型,并提供了基本的低級别同步原語(主要是 sync 包中的互斥鎖、條件變量、讀寫鎖、原子操作等)。

繼續閱讀