Go 併發

Go 併發

Go 語言支援併發,我們只需要通過 go 關鍵字來開啟 goroutine 即可。

goroutine 是輕量級執行緒,goroutine 的排程是由 Golang 執行時進行管理的。

goroutine 語法格式:

go 函式名( 引數列表 )

例如:

go f(x, y, z)

開啟一個新的 goroutine:

f(x, y, z)

Go 允許使用 go 語句開啟一個新的執行期執行緒, 即 goroutine,以一個不同的、新建立的 goroutine 來執行一個函式。 同一個程式中的所有 goroutine 共享同一個地址空間。

例項

package main

import (
        "fmt"
        "time"
)

func say(s string) {
        for i := 0; i < 5; i++ {
                time.Sleep(100 * time.Millisecond)
                fmt.Println(s)
        }
}

func main() {
        go say("world")
        say("hello")
}

執行以上程式碼,你會看到輸出的 hello 和 world 是沒有固定先後順序。因為它們是兩個 goroutine 在執行:

world
hello
hello
world
world
hello
hello
world
world
hello

通道(channel)

通道(channel)是用來傳遞資料的一個數據結構。

通道可用於兩個 goroutine 之間通過傳遞一個指定型別的值來同步執行和通訊。操作符 <- 用於指定通道的方向,傳送或接收。如果未指定方向,則為雙向通道。

ch <- v    // 把 v 傳送到通道 ch
v := <-ch  // 從 ch 接收資料
           // 並把值賦給 v

宣告一個通道很簡單,我們使用chan關鍵字即可,通道在使用前必須先建立:

ch := make(chan int)

注意:預設情況下,通道是不帶緩衝區的。傳送端傳送資料,同時必須有接收端相應的接收資料。

以下例項通過兩個 goroutine 來計算數字之和,在 goroutine 完成計算後,它會計算兩個結果的和:

例項

package main

import "fmt"

func sum(s []int, c chan int) {
        sum := 0
        for _, v := range s {
                sum += v
        }
        c <- sum // 把 sum 傳送到通道 c
}

func main() {
        s := []int{7, 2, 8, -9, 4, 0}

        c := make(chan int)
        go sum(s[:len(s)/2], c)
        go sum(s[len(s)/2:], c)
        x, y := <-c, <-c // 從通道 c 中接收

        fmt.Println(x, y, x+y)
}

輸出結果為:

-5 17 12

通道緩衝區

通道可以設定緩衝區,通過 make 的第二個引數指定緩衝區大小:

ch := make(chan int, 100)

帶緩衝區的通道允許傳送端的資料傳送和接收端的資料獲取處於非同步狀態,就是說傳送端傳送的資料可以放在緩衝區裡面,可以等待接收端去獲取資料,而不是立刻需要接收端去獲取資料。

不過由於緩衝區的大小是有限的,所以還是必須有接收端來接收資料的,否則緩衝區一滿,資料傳送端就無法再發送資料了。

注意:如果通道不帶緩衝,傳送方會阻塞直到接收方從通道中接收了值。如果通道帶緩衝,傳送方則會阻塞直到傳送的值被拷貝到緩衝區內;如果緩衝區已滿,則意味著需要等待直到某個接收方獲取到一個值。接收方在有值可以接收之前會一直阻塞。

例項

package main

import "fmt"

func main() {
    // 這裡我們定義了一個可以儲存整數型別的帶緩衝通道
        // 緩衝區大小為2
        ch := make(chan int, 2)

        // 因為 ch 是帶緩衝的通道,我們可以同時傳送兩個資料
        // 而不用立刻需要去同步讀取資料
        ch <- 1
        ch <- 2

        // 獲取這兩個資料
        fmt.Println(<-ch)
        fmt.Println(<-ch)
}

執行輸出結果為:

1
2

Go 遍歷通道與關閉通道

Go 通過 range 關鍵字來實現遍歷讀取到的資料,類似於與陣列或切片。格式如下:

v, ok := <-ch

如果通道接收不到資料後 ok 就為 false,這時通道就可以使用 close() 函式來關閉。

例項

package main

import (
        "fmt"
)

func fibonacci(n int, c chan int) {
        x, y := 0, 1
        for i := 0; i < n; i++ {
                c <- x
                x, y = y, x+y
        }
        close(c)
}

func main() {
        c := make(chan int, 10)
        go fibonacci(cap(c), c)
        // range 函式遍歷每個從通道接收到的資料,因為 c 在傳送完 10 個
        // 資料之後就關閉了通道,所以這裡我們 range 函式在接收到 10 個數據
        // 之後就結束了。如果上面的 c 通道不關閉,那麼 range 函式就不
        // 會結束,從而在接收第 11 個數據的時候就阻塞了。
        for i := range c {
                fmt.Println(i)
        }
}

執行輸出結果為:

0
1
1
2
3
5
8
13
21
34