Go語言CSP理論並發方案中的channel

jodkreaper 發佈 2024-04-09T12:44:58.469969+00:00

Go 語言的 CSP 模型的實現包含兩個主要組成部分:一個是 Goroutine,它是 Go 應用並發設計的基本構建與執行單元;另一個就是 channel,它在並發模型中扮演著重要的角色。

Go 語言的 CSP 模型的實現包含兩個主要組成部分:一個是 Goroutine,它是 Go 應用並發設計的基本構建與執行單元;另一個就是 channel,它在並發模型中扮演著重要的角色。channel 既可以用來實現 Goroutine 間的通信,還可以實現 Goroutine 間的同步

channel 作為一等公民我們可以像使用普通變量那樣使用 channel,比如,定義 channel 類型變量、給 channel 變量賦值、將 channel 作為參數傳遞給函數 / 方法、將 channel 作為返回值從函數 / 方法中返回,甚至將 channel 發送到其他 channel 中。

創建 channel

和切片、結構體、map 等一樣,channel 也是一種複合數據類型。也就是說,我們在聲明一個 channel 類型變量時,必須給出其具體的元素類型,比如下面的代碼這樣:


var ch chan int

上面,聲明了一個元素為 int 類型的 channel 類型變量 ch。

如果 channel 類型變量在聲明時沒有被賦予初值,那麼它的默認值為 nil。並且,和其他複合數據類型支持使用複合類型字面值作為變量初始值不同,為 channel 類型變量賦初值的唯一方法就是使用 make 這個 Go 預定義的函數,比如下面代碼:


ch1 := make(chan int)   
ch2 := make(chan int, 5) 

這裡,我們聲明了兩個元素類型為 int 的 channel 類型變量 ch1 和 ch2,並給這兩個變量賦了初值。但我們看到,兩個變量的賦初值操作使用的 make 調用的形式有所不同。

第一行我們通過make(chan T)創建的、元素類型為 T 的 channel 類型,是無緩衝 channel,而第二行中通過帶有 capacity 參數的make(chan T, capacity)創建的元素類型為 T、緩衝區長度為 capacity 的 channel 類型,是帶緩衝 channel。這兩種類型的變量關於發送(send)與接收(receive)的特性是不同的。

發送與接收

Go 提供了<-操作符用於對 channel 類型變量進行發送與接收操作:


ch1 <- 13    // 將整型字面值13發送到無緩衝channel類型變量ch1中
n := <- ch1  // 從無緩衝channel類型變量ch1中接收一個整型值存儲到整型變量n中
ch2 <- 17    // 將整型字面值17發送到帶緩衝channel類型變量ch2中
m := <- ch2  // 從帶緩衝channel類型變量ch2中接收一個整型值存儲到整型變量m中

channel 是用於 Goroutine 間通信的,所以絕大多數對 channel 的讀寫都被分別放在了不同的 Goroutine 中。

由於無緩衝 channel 的運行時層實現不帶有緩衝區,所以 Goroutine 對無緩衝 channel 的接收和發送操作是同步的。也就是說,對同一個無緩衝 channel,只有對它進行接收操作的 Goroutine 和對它進行發送操作的 Goroutine 都存在的情況下,通信才能得以進行,否則單方面的操作會讓對應的 Goroutine 陷入掛起狀態,比如下面示例代碼:


func main() {
    ch1 := make(chan int)
    ch1 <- 13 // fatal error: all goroutines are asleep - deadlock!
    n := <-ch1
    println(n)
}

在這個示例中,我們創建了一個無緩衝的 channel 類型變量 ch1,對 ch1 的讀寫都放在了一個 Goroutine 中。運行這個示例,我們就會得到 fatal error,提示我們所有 Goroutine 都處於休眠狀態,程序處於死鎖狀態。要想解除這種錯誤狀態,我們只需要將接收操作,或者發送操作放到另外一個 Goroutine 中就可以了,比如下面代碼:


func main() {
    ch1 := make(chan int)
    go func() {
        ch1 <- 13 // 將發送操作放入一個新goroutine中執行
    }()
    n := <-ch1
    println(n)
}

由此,我們可以得出結論:對無緩衝 channel 類型的發送與接收操作,一定要放在兩個不同的 Goroutine 中進行,否則會導致 deadlock

和無緩衝 channel 相反,帶緩衝 channel 的運行時層實現帶有緩衝區,因此,對帶緩衝 channel 的發送操作在緩衝區未滿、接收操作在緩衝區非空的情況下是異步的(發送或接收不需要阻塞等待)。

也就是說,對一個帶緩衝 channel 來說,在緩衝區未滿的情況下,對它進行發送操作的 Goroutine 並不會阻塞掛起;在緩衝區有數據的情況下,對它進行接收操作的 Goroutine 也不會阻塞掛起。但當緩衝區滿了的情況下,對它進行發送操作的 Goroutine 就會阻塞掛起;當緩衝區為空的情況下,對它進行接收操作的 Goroutine 也會阻塞掛起。


ch2 := make(chan int, 1)
n := <-ch2 // 由於此時ch2的緩衝區中無數據,因此對其進行接收操作將導致goroutine掛起

ch3 := make(chan int, 1)
ch3 <- 17  // 向ch3發送一個整型數17
ch3 <- 27  // 由於此時ch3中緩衝區已滿,再向ch3發送數據也將導致goroutine掛起

使用操作符<-,我們還可以聲明只發送 channel 類型(send-only)和只接收 channel 類型(recv-only),我們接著看下面這個例子:


ch1 := make(chan<- int, 1) // 只發送channel類型
ch2 := make(<-chan int, 1) // 只接收channel類型

<-ch1       // invalid operation: <-ch1 (receive from send-only type chan<- int)
ch2 <- 13   // invalid operation: ch2 <- 13 (send to receive-only type <-chan int)

這個例子中看到,試圖從一個只發送 channel 類型變量中接收數據,或者向一個只接收 channel 類型發送數據,都會導致編譯錯誤。通常只發送 channel 類型和只接收 channel 類型,會被用作函數的參數類型或返回值,用於限制對 channel 內的操作,或者是明確可對 channel 進行的操作的類型

關閉 channel


func produce(ch chan<- int) {
    for i := 0; i < 10; i++ {
        ch <- i + 1
        time.Sleep(time.Second)
    }
    close(ch)
}

func consume(ch <-chan int) {
    for n := range ch {
        println(n)
    }
}

func main() {
    ch := make(chan int, 5)
    var wg sync.WaitGroup
    wg.Add(2)
    go func() {
        produce(ch)
        wg.Done()
    }()

    go func() {
        consume(ch)
        wg.Done()
    }()

    wg.Wait()
}

produce 函數在發送完數據後,調用 Go 內置的 close 函數關閉了 channel。channel 關閉後,所有等待從這個 channel 接收數據的操作都將返回。

採用不同接收語法形式的語句,在 channel 被關閉後的返回值的情況:


n := <- ch      // 當ch被關閉後,n將被賦值為ch元素類型的零值
m, ok := <-ch   // 當ch被關閉後,m將被賦值為ch元素類型的零值, ok值為false
for v := range ch { // 當ch被關閉後,for range循環結束
    ... ...
}

通過「comma, ok」慣用法或 for range 語句,我們可以準確地判定 channel 是否被關閉。而單純採用n := <-ch形式的語句,我們就無法判定從 ch 返回的元素類型零值,究竟是不是因為 channel 被關閉後才返回的。

注意:

channel 的一個使用慣例,那就是發送端負責關閉 channel。這是因為發送端沒有像接受端那樣的、可以安全判斷 channel 是否被關閉了的方法。同時,一旦向一個已經關閉的 channel 執行發送操作,這個操作就會引發 panic。

關鍵字: