1. 程式人生 > 其它 >GO協程

GO協程

協程(goroutine)

我們稱之為 Go 協程是因為現有的術語 — 執行緒、協程、程序等等 — 無法準確傳達它的含義。 Go 協程具有簡單的模型:它是與其它 Go 協程併發執行在同一地址空間的函式。它是輕量級的, 所有消耗幾乎就只有棧空間的分配。而且棧最開始是非常小的,所以它們很廉價, 僅在需要時才會隨著堆空間的分配(和釋放)而變化。

Go 協程在多執行緒作業系統上可實現多路複用,因此若一個執行緒阻塞,比如說等待 I/O, 那麼其它的執行緒就會執行。Go 協程的設計隱藏了執行緒建立和管理的諸多複雜性。

在函式或方法前新增 go 關鍵字能夠在新的 Go 協程中呼叫它。當呼叫完成後, 該 Go 協程也會安靜地退出。(效果有點像 Unix Shell 中的 &

符號,它能讓命令在後臺執行。)

go list.Sort()  // 同時執行 list.Sort ; 不需要等待

匿名函式在協程中呼叫非常方便:

func Announce(message string, delay time.Duration) {
    go func() {
        time.Sleep(delay)
        fmt.Println(message)
    }()  // 注意括號 - 必須呼叫函式
}

在 Go 中,匿名函式都是閉包:其實現在保證了函式內引用變數的生命週期與函式的活動時間相同。

這些函式沒什麼實用性,因為它們沒有實現完成時的訊號處理。因此,我們需要通道。

通道

通道與對映一樣,也需要通過 make 來分配記憶體。其結果值充當了對底層資料結構的引用。 若提供了一個可選的整數形參,它就會為該通道設定緩衝區大小。預設值是零,表示不帶緩衝的或同步的通道。

ci := make(chan int)            // 整數無緩衝通道
cs := make(chan *os.File, 100)  // 指向檔案的指標的緩衝通道

無緩衝通道在通訊時會同步交換資料,它能確保(兩個 Go 協程的)計算處於確定狀態。

我們在後臺啟動了排序操作。 通道使得啟動的 Go 協程等待排序完成。

c := make(chan int)  // 建立一個無緩衝的型別為整型的 channel 。
//用 goroutine 開始排序;當它完成時,會在通道上發訊號。
go func() {
    list.Sort()
    c <- 1  // 傳送一個訊號,這個值並沒有具體意義
}()
doSomethingForAWhile()
<-c   // 等待 sort 執行完成,然後從 channel 取值

接收者在收到資料前會一直阻塞。若通道是不帶緩衝的,那麼在接收者收到值前, 傳送者會一直阻塞;若通道是帶緩衝的,則傳送者僅在值被複制到緩衝區前阻塞; 若緩衝區已滿,傳送者會一直等待直到某個接收者取出一個值為止。

帶緩衝的通道可被用作訊號量,例如限制吞吐量。

var sem = make(chan int, MaxOutstanding)

func handle(r *Request) {
    sem <- 1    // 等待活動佇列清空。
    process(r)  // 可能需要很長時間。
    <-sem       // 完成;使下一個請求可以執行。
}

func Serve(queue chan *Request) {
    for {
        req := <-queue
        go handle(req)  // 無需等待 handle 結束。
    }
}

在此例中,進入的請求會被傳遞給 handle,它向通道內傳送一個值,處理請求後將值從通道中取回,以便讓該 “訊號量” 準備迎接下一次請求。通道緩衝區的容量決定了同時呼叫 process 的數量上限,因此我們在初始化時首先要填充至它的容量上限。

一旦有 MaxOutstanding 個處理程式正在執行 process,緩衝區已滿的通道的操作都暫停接收更多操作,直到至少一個程式完成並從緩衝區接收。

並行化

這些設計的另一個應用是在多 CPU 核心上實現平行計算。如果計算過程能夠被分為幾塊 可獨立執行的過程,它就可以在每塊計算結束時向通道傳送訊號,從而實現並行處理。

如我們在對一系列向量項進行極耗資源的操作, 而每個項的值計算是完全獨立的。

type Vector []float64

// 將此操應用至 v[i], v[i+1] ... 直到 v[n-1]
func (v Vector) DoSome(i, n int, u Vector, c chan int) {
    for ; i < n; i++ {
        v[i] += u.Op(v[i])
    }
    c <- 1    // signal that this piece is done
}

我們在迴圈中啟動了獨立的處理塊,每個 CPU 將執行一個處理。 它們有可能以亂序的形式完成並結束,但這沒有關係; 我們只需在所有 Go 協程開始後接收,並統計通道中的完成訊號即可。

const numCPU = 4 // CPU 核心數

func (v Vector) DoAll(u Vector) {
    c := make(chan int, numCPU)  // 緩衝區是可選的,但明顯用上更好
    for i := 0; i < numCPU; i++ {
        go v.DoSome(i*len(v)/numCPU, (i+1)*len(v)/numCPU, u, c)
    }
    // 排空通道。
    for i := 0; i < numCPU; i++ {
        <-c    // 等待任務完成
    }
    // 一切完成
}

注意不要混淆併發(concurrency)和並行(parallelism)的概念:
併發是用可獨立執行元件構造程式的方法, 而並行則是為了效率在多 CPU 上平行地進行計算。儘管 Go 的併發特效能夠讓某些問題更易構造成平行計算, 但 Go 仍然是種併發而非並行的語言,且 Go 的模型並不適合所有的並行問題。