前言

Go 語言中並發程序可以用兩種方式來實現。

一種是 goroutinechannel,其支持 “交談循序程式”(communicating sequential processes)或被簡稱為 CSP。

CSP 是一個現代的並發程式模型,在這種程式模型中值會在不同的運行實例(goroutine)中傳遞,儘管大多數情況下被限制在單一實例中。

另一種是傳統的並發模型,多線程共享內存(基於共享變量的並發) , 會在後續單獨闡述。

goroutine

goroutine 是一個輕量級的線程 , 它與線程的區別是線程是操作系統中對於一個獨立運行實例的描述,不同的操作系統中,線程的實現也不盡相同;

對於 goroutine,操作系統並不知道它的存在 , goroutine 的調度是 Go 語言的運行時進行管理的。

啟動線程雖然比進程使用的資源少 , 但依然需要上下文切換等大量工作 , Go 語言有自己的調度器 , 許多 goroutine 的數據都是共享的 , 因此 goroutine 之間的切換會快很多 , 啟動 goroutine 所耗費的資源也很少。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package main

import (
    "fmt"
    "strconv"
)

func Info(name string)  {
    for i := 0; i < 3; i++ {
        fmt.Println(name + ":" + strconv.Itoa(i))
    }
}

func main()  {
    Info("info")

    go Info("goroutine1")

    go func(name string) {
        fmt.Println(name)
    }("goroutine2")

    var input string
    fmt.Scanln(&input)
    fmt.Println("done")
}

channel

channel 被稱為通道 , 是連接並發 goroutine 的管道 , 可以從一個 goroutine 向通道發送值 , 並在另一個 goroutine 中接收到這些值 。

每個 channel 都有一個特殊的類型 , 也就是 channel 可發送數據的類型 。 一個可以發送 int 類型數據的 channel 一般寫為 chan int

一個 channel 有發送和接收兩個主要操作 , 都是通信行為 。

一個發送語句將一個值從一個 goroutine 通過 channel 發送到另一個執行接收操作的 goroutine 。

發送和接收兩個操作都是用 <- 運算符 。

在發送語句中 , <- 運算符分割 channel 和要發送的值 ; 在接收語句中 , <- 運算符寫在 channel 對象之前 , 一個不使用接收結果的接收操作也是合法的。

channel 的發送操作將導致發送者 goroutine 阻塞 , 直到另一個 goroutine 在相同的 channel 上執行接收操作 , 當發送的值通過 channel 成功傳輸之後 , 兩個 goroutine 可以繼續執行後面的語句 。

反之 , 如果接收操作先發生 , 那麼接收者 goroutine 也將阻塞 , 直到有另一個 goroutine 在相同的 Channels 上執行發送操作。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package main

import (
    "fmt"
    "strconv"
)

func Info(i int, ch chan string)  {
    msg := "訊息" + strconv.Itoa(i)
    ch <- msg
}

func main()  {
    chs := make([]chan string, 3)
    for i := 0; i < 3; i++ {
        chs[i] = make(chan string)
        go Info(i, chs[i])
    }

    for num, ch := range chs {
        msg := <- ch
        fmt.Println(num, msg)
    }

    fmt.Println("Done")
}

channel 還支持 close 操作 , 用於關閉 channel , 隨後對基於該 channel 的任何發送操作都將導致 Panic 異常 。

對一個已經被 close 過的 channel 接收操作依然可以接受到之前已經成功發送的數據 ; 如果 channel 中已經沒有數據的話將產生一個零值(nil)的數據 。

1
close(ch)

帶緩存的 channel

帶緩存的 channel 內部有一個元素隊列 。 隊列的最大容量是在調用 make 函數創建 channel 時通過第二個參數指定的 。 下面的語法創建了一個可以持有三個字符串元素的帶緩存 channel

1
ch = make(chan string, 3)

向緩存 channel 的發送操作就是向內部緩存隊列的尾部插入元素 , 接收操作則是從隊列的頭部刪除元素 。

如果內部緩存隊列是滿的 , 那麼發送操作將阻塞直到另一個 goroutine 執行接收操作而釋放了新的隊列空間。

相反,如果 channel 是空的 , 接收操作將阻塞直到有另一個 goroutine 執行發送操作而向隊列插入元素 。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
package main

import (
    "fmt"
)

func main()  {
    ch := make(chan string, 3)

    ch <- "A"
    ch <- "B"
    ch <- "C"

    fmt.Println(<-ch)
    fmt.Println(<-ch)
    fmt.Println(<-ch)
}

select

Go 語言的 select 的功能和 select、poll、epoll 相似 , 就是監聽 IO 操作 , 當 IO 操作發生時 , 觸發響應的動作 。

select 語法和 switch 相似 , 也會有幾個 casedefault 分支 , 每一個 case 代表一個通信操作(在某個 channel 上進行發送或者接收)並且會包含一些語句組成一個語句塊。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package main

import (
    "fmt"
    "strconv"
)

func main()  {
    ch1 := make(chan string)
    ch2 := make(chan string)

    go func() {
        ch1 <- "I am from ch1"
    }()

    go func() {
        ch2 <- "I am from ch2"
    }()

    for i := 0; i < 2; i++ {
        select {
        case msg1 := <- ch1:
            fmt.Println("received-"+strconv.Itoa(i), msg1)
        case msg2 := <- ch2:
            fmt.Println("received-"+strconv.Itoa(i), msg2)
        }
    }
}

超時控制

select 是用來讓我們的程序監聽多個文件句柄的狀態變化的處理機制 。

當發起一些阻塞的請求後 , 可以用 select 機制輪詢掃描文件句柄 , 直到被監視的文件句柄有一個或多個發生了狀態改變。

channel 在系統層面來說也是個文件描述符 , 在 Go 語言中我們可以用 goroutine 並發執行任務 , 接著使用 select 來監視每個任務的 channel 情況 。

如果這幾個任務都長時間沒有回复 channel 信息 , 並且我們又有超時的需求 , 那麼我們可以使用一個 goroutine 來設置超時機制 , 具體做法就是啟動 sleep 並且在 sleep 之後回复 channel 信號。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package main

import (
    "fmt"
    "time"
)

func main()  {
    ch := make(chan string)
    timeout := make(chan bool)

    go func() {
        time.Sleep(5 * time.Second)
        timeout <- true
    }()

    go func() {
        time.Sleep(10 * time.Second)
        ch <- "Hello World"
    }()

    select {
    case msg := <- ch:
        fmt.Println(msg)
    case <- timeout:
        fmt.Println("task is timeout")
    }
}