golang 中 channel 的詳細使用、使用注意事項及死鎖分析
- 2022 年 3 月 16 日
- 筆記
- [01]Golang, Channel, Go, golang
目錄
1.什麼是 channel,介紹管道
2.channel 的基本使用
3.channel 的使用場景
4.使用 channel的注意事項及死鎖分析
什麼是 channel 管道
它是一個數據管道,可以往裡面寫數據,從裡面讀數據。
channel 是 goroutine 之間數據通訊橋樑,而且是執行緒安全的。
channel 遵循先進先出原則。
寫入,讀出數據都會加鎖。
channel 可以分為 3 種類型:
-
只讀 channel,單向 channel
-
只寫 channel,單向 channel
-
可讀可寫 channel
channel 還可按是否帶有緩衝區分為:
-
帶緩衝區的 channel,定義了緩衝區大小,可以存儲多個數據
-
不帶緩衝區的 channel,只能存一個數據,並且只有當該數據被取出才能存下一個數據
channel 的基本使用
定義和聲明
// 只讀 channel
var readOnlyChan <-chan int // channel 的類型為 int
// 只寫 channel
var writeOnlyChan chan<- int
// 可讀可寫
var ch chan int
// 或者使用 make 直接初始化
readOnlyChan1 := make(<-chan int, 2) // 只讀且帶快取區的 channel
readOnlyChan2 := make(<-chan int) // 只讀且不帶快取區 channel
readOnlyChan3 := make(chan<- int, 4) // 只寫且帶快取區 channel
readOnlyChan4 := make(chan<- int) // 只寫且不帶快取區 channel
ch := make(chan int, 10) // 可讀可寫且帶快取區
ch <- 20 // 寫數據
i := <-ch // 讀數據
i, ok := <-ch // 還可以判斷讀取的數據
chan_var.go
package main
import (
"fmt"
)
func main() {
// var 聲明一個 channel,它的零值是nil
var ch chan int
fmt.Printf("var: the type of ch is %T \n", ch)
fmt.Printf("var: the val of ch is %v \n", ch)
if ch == nil {
// 也可以用make聲明一個channel,它返回的值是一個記憶體地址
ch = make(chan int)
fmt.Printf("make: the type of ch is %T \n", ch)
fmt.Printf("make: the val of ch is %v \n", ch)
}
ch2 := make(chan string, 10)
fmt.Printf("make: the type of ch2 is %T \n", ch2)
fmt.Printf("make: the val of ch2 is %v \n", ch2)
}
// 輸出:
// var: the type of ch is chan int
// var: the val of ch is <nil>
// make: the type of ch is chan int
// make: the val of ch is 0xc000048060
// make: the type of ch2 is chan string
// make: the val of ch2 is 0xc000044060
操作channel的3種方式
操作 channel 一般有如下三種方式:
-
讀 <-ch
-
寫 ch<-
-
關閉 close(ch)
操作 | nil的channel | 正常channel | 已關閉的channel |
---|---|---|---|
讀 <-ch | 阻塞 | 成功或阻塞 | 讀到零值 |
寫 ch<- | 阻塞 | 成功或阻塞 | panic |
關閉 close(ch) | panic | 成功 | panic |
注意 對於 nil channel 的情況,有1個特殊場景:
當 nil channel 在 select 的某個 case 中時,這個 case 會阻塞,但不會造成死鎖。
單向 channel
單向 channel:只讀和只寫的 channel
chan_uni.go
package main
import "fmt"
func main() {
// 單向 channel,只寫channel
ch := make(chan<- int)
go testData(ch)
fmt.Println(<-ch)
}
func testData(ch chan<- int) {
ch <- 10
}
// 運行輸出
// ./chan_uni.go:9:14: invalid operation: <-ch (receive from send-only type chan<- int)
// 報錯,它是一個只寫 send-only channel
把上面程式碼main()函數里初始化的單向channel,修改為可讀可寫channel,再運行
chan_uni2.go
package main
import "fmt"
func main() {
// 把上面程式碼main()函數初始化的單向 channel 修改為可讀可寫的 channel
ch := make(chan int)
go testData(ch)
fmt.Println(<-ch)
}
func testData(ch chan<- int) {
ch <- 10
}
// 運行輸出:
// 10
// 沒有報錯,可以正常輸出結果
帶緩衝和不帶緩衝的 channel
不帶緩衝區 channel
chan_unbuffer.go
package main
import "fmt"
func main() {
ch := make(chan int) // 無緩衝的channel
go unbufferChan(ch)
for i := 0; i < 10; i++ {
fmt.Println("receive ", <-ch) // 讀出值
}
}
func unbufferChan(ch chan int) {
for i := 0; i < 10; i++ {
fmt.Println("send ", i)
ch <- i // 寫入值
}
}
// 輸出
send 0
send 1
receive 0
receive 1
send 2
send 3
receive 2
receive 3
send 4
send 5
receive 4
receive 5
send 6
send 7
receive 6
receive 7
send 8
send 9
receive 8
receive 9
帶緩衝區 channel
chan_buffer.go
package main
import (
"fmt"
)
func main() {
ch := make(chan string, 3)
ch <- "tom"
ch <- "jimmy"
ch <- "cate"
fmt.Println(<-ch)
fmt.Println(<-ch)
fmt.Println(<-ch)
}
// 運行輸出:
// tom
// jimmy
// cate
再看一個例子:chan_buffer2.go
package main
import (
"fmt"
"time"
)
var c = make(chan int, 5)
func main() {
go worker(1)
for i := 1; i < 10; i++ {
c <- i
fmt.Println(i)
}
}
func worker(id int) {
for {
_ = <-c
}
}
// 運行輸出:
// 1
// 2
// 3
// 4
// 5
// 6
// 7
// 8
// 9
判斷 channel 是否關閉
if v, ok := <-ch; ok {
fmt.Println(ch)
}
說明:
- ok 為 true,讀到數據,且管道沒有關閉
- ok 為 false,管道已關閉,沒有數據可讀
讀已經關閉的 channel 會讀到零值,如果不確定 channel 是否關閉,可以用這種方法來檢測。
range and close
range 可以遍曆數組,map,字元串,channel等。
一個發送者可以關閉 channel,表明沒有任何數據發送給這個 channel 了。接收者也可以測試channel是否關閉,通過 v, ok := <-ch
表達式中的 ok 值來判斷 channel 是否關閉。上一節已經說明 ok 為 false 時,表示 channel 沒有接收任何數據,它已經關閉了。
注意:僅僅只能是發送者關閉一個 channel,而不能是接收者。給已經關閉的 channel 發送數據會導致 panic。
Note: channels 不是文件,你通常不需要關閉他們。那什麼時候需要關閉?當要告訴接收者沒有值發送給 channel 了,這時就需要了。
比如終止 range 循環。
當 for range 遍歷 channel 時,如果發送者沒有關閉 channel 或在 range 之後關閉,都會導致 deadlock(死鎖)。
下面是一個會產生死鎖的例子:
package main
import "fmt"
func main() {
ch := make(chan int)
go func() {
for i := 0; i < 10; i++ {
ch <- i
}
}()
for val := range ch {
fmt.Println(val)
}
close(ch) // 這裡關閉channel已經」通知「不到range了,會觸發死鎖。
// 不管這裡是否關閉channel,都會報死鎖,close(ch)的位置就不對。
// 且關閉channel的操作者也錯了,只能是發送者關閉channel
}
// 運行程式輸出
// 0
// 1
// 2
// 3
// 4
// 5
// 6
// 7
// 8
// 9
// fatal error: all goroutines are asleep - deadlock!
改正也很簡單,把 close(ch)
移到 go func(){}()
里,程式碼如下
go func() {
for i := 0; i < 10; i++ {
ch <- i
}
close(ch)
}()
這樣程式就可以正常運行,不會報 deadlock 的錯誤了。
把上面程式換一種方式來寫,chan_range.go
package main
import (
"fmt"
)
func main() {
ch := make(chan int)
go test(ch)
for val := range ch { //
fmt.Println("get val: ", val)
}
}
func test(ch chan int) {
for i := 0; i < 5; i++ {
ch <- i
}
close(ch)
}
// 運行輸出:
// get val: 0
// get val: 1
// get val: 2
// get val: 3
// get val: 4
發送者關閉 channel 時,for range 循環自動退出。
for 讀取channel
用 for 來不停循環讀取 channel 里的數據。
把上面的 range 程式修改下,chan_for.go
package main
import (
"fmt"
)
func main() {
ch := make(chan int)
go test(ch)
for {
val, ok := <-ch
if ok == false {// ok 為 false,沒有數據可讀
break // 跳出循環
}
fmt.Println("get val: ", val)
}
}
func test(ch chan int) {
for i := 0; i < 5; i++ {
ch <- i
}
close(ch)
}
// 運行輸出:
// get val: 0
// get val: 1
// get val: 2
// get val: 3
// get val: 4
select 使用
例子 chan_select.go
package main
import "fmt"
// //go.dev/tour/concurrency/5
func fibonacci(ch, quit chan int) {
x, y := 0, 1
for {
select {
case ch <- x:
x, y = y, x+y
case <-quit:
fmt.Println("quit")
return
}
}
}
func main() {
ch := make(chan int)
quit := make(chan int)
go func() {
for i := 0; i < 10; i++ {
fmt.Println(<-ch)
}
quit <- 0
}()
fibonacci(ch, quit)
}
// 運行輸出:
// 0
// 1
// 1
// 2
// 3
// 5
// 8
// 13
// 21
// 34
// quit
channel 的一些使用場景
1. 作為goroutine的數據傳輸管道
package main
import "fmt"
// //go.dev/tour/concurrency/2
func sums(s []int, c chan int) {
sum := 0
for _, v := range s {
sum += v
}
c <- sum
}
func main() {
s := []int{7, 2, 8, -9, 4, 0}
c := make(chan int)
go sums(s[:len(s)/2], c)
go sums(s[len(s)/2:], c)
x, y := <-c, <-c // receive from c
fmt.Println(x, y, x+y)
}
用 goroutine 和 channel 分批求和
2. 同步的channel
沒有緩衝區的 channel 可以作為同步數據的管道,起到同步數據的作用。
對沒有緩衝區的 channel 操作時,發送的 goroutine 和接收的 goroutine 需要同時準備好,也就是發送和接收需要一一配對,才能完成發送和接收的操作。
如果兩方的 goroutine 沒有同時準備好,channel 會導致先執行發送或接收的 goroutine 阻塞等待。這就是沒有緩衝區的 channel 作為數據同步的作用。
gobyexample 中的一個例子:
package main
import (
"fmt"
"time"
)
////gobyexample.com/channel-synchronization
func worker(done chan bool) {
fmt.Println("working...")
time.Sleep(time.Second)
fmt.Println("done")
done <- true
}
func main() {
done := make(chan bool, 1)
go worker(done)
<-done
}
注意:同步的 channel 千萬不要在同一個 goroutine 協程里發送和接收數據。可能導致deadlock死鎖。
3. 非同步的channel
有緩衝區的 channel 可以作為非同步的 channel 使用。
有緩衝區的 channel 也有操作注意事項:
如果 channel 中沒有值了,channel 為空了,那麼接收者會被阻塞。
如果 channel 中的緩衝區滿了,那麼發送者會被阻塞。
注意:有緩衝區的 channel,用完了要 close,不然處理這個channel 的 goroutine 會被阻塞,形成死鎖。
package main
import (
"fmt"
)
func main() {
ch := make(chan int, 4)
quitChan := make(chan bool)
go func() {
for v := range ch {
fmt.Println(v)
}
quitChan <- true // 通知用的channel,表示這裡的程式已經執行完了
}()
ch <- 1
ch <- 2
ch <- 3
ch <- 4
ch <- 5
close(ch) // 用完關閉channel
<-quitChan // 接到channel通知後解除阻塞,這也是channel的一種用法
}
4.channel 超時處理
channel 結合 time 實現超時處理。
當一個 channel 讀取數據超過一定時間還沒有數據到來時,可以得到超時通知,防止一直阻塞當前 goroutine。
chan_timeout.go
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan int)
quitChan := make(chan bool)
go func() {
for {
select {
case v := <-ch:
fmt.Println(v)
case <-time.After(time.Second * time.Duration(3)):
quitChan <- true
fmt.Println("timeout, send notice")
return
}
}
}()
for i := 0; i < 4; i++ {
ch <- i
}
<-quitChan // 輸出值,相當於收到通知,解除主程阻塞
fmt.Println("main quit out")
}
使用 channel 的注意事項及死鎖分析
未初始化的 channel 讀寫關閉操作
- 讀:未初始化的channel,讀取裡面的數據時,會造成死鎖deadlock
var ch chan int
<-ch // 未初始化channel讀數據會死鎖
- 寫:未初始化的channel,往裡面寫數據時,會造成死鎖deadlock
var ch chan int
ch<- // 未初始化channel寫數據會死鎖
- 關閉:未初始化的channel,關閉該channel時,會panic
var ch chan int
close(ch) // 關閉未初始化channel,觸發panic
已初始化的 channel 讀寫關閉操作
-
已初始化,沒有緩衝區的channel
// 程式碼片段1 func main() { ch := make(chan int) ch <- 4 }
程式碼片段1:沒有緩衝channel,且只有寫入沒有讀取,會產生死鎖
// 程式碼片段2 func main() { ch := make(chan int) val, ok := <-ch }
程式碼片段2:沒有緩衝channel,且只有讀取沒有寫入,會產生死鎖
// 程式碼片段3 func main() { ch := make(chan int) val, ok := <-ch if ok { fmt.Println(val) } ch <- 10 // 這裡進行寫入。但是前面已經產生死鎖了 }
程式碼片段3:沒有緩衝channel,既有寫入也有讀出,但是在程式碼
val, ok := <-c
處已經產生死鎖了。下面程式碼執行不到。// 程式碼片段4 func main() { ch := make(chan int) ch <- 10 go readChan(ch) time.Sleep(time.Second * 2) } func readChan(ch chan int) { for { val, ok := <-ch fmt.Println("read ch: ", val) if !ok { break } } }
程式碼片段4:沒有緩衝channel,既有寫入也有讀出,但是運行程式後,報錯
fatal error: all goroutines are asleep - deadlock!
。這是因為往 channle 里寫入數據的程式碼
ch <- 10
,這裡寫入數據時就已經產生死鎖了。把ch<-10
和go readChan(ch)
調換位置,程式就能正常運行,不會產生死鎖。// 程式碼片段5 func main() { ch := make(chan int) go writeChan(ch) for { val, ok := <-ch fmt.Println("read ch: ", val) if !ok { break } } time.Sleep(time.Second) fmt.Println("end") } func writeChan(ch chan int) { for i := 0; i < 4; i++ { ch <- i } }
程式碼片段5:沒有緩衝的channel,既有寫入,也有讀出,與上面幾個程式碼片段不同的是,寫入channel的數據不是一個。
思考一下,這個程式會產生死鎖嗎?10 秒時間思考下,先不要看下面。
也會產生死鎖,它會輸出完數據後,報錯
fatal error: all goroutines are asleep - deadlock!
。為什麼呢?這個程式片段,既有讀也有寫而且先開一個goroutine寫數據,為什麼會死鎖?
原因在於
main()
里的for
循環。可能你會問,不是有break
跳出for
循環嗎?程式碼是寫了,但是程式並沒有執行到這裡。因為
for
會不停的循環,而val, ok := <-ch
, 這裡ok
值一直是 true,因為程式里並沒有哪裡關閉 channel 啊。你們可以列印這個ok
值看一看是不是一直是 true。當for
循環把 channel 里的值讀取完了後,程式再次運行到val, ok := <-ch
時,產生死鎖,因為 channel 里沒有數據了。找到原因了,那解決辦法也很簡單,在
writeChan
函數里關閉 channel,加上程式碼close(ch)
。告訴for
我寫完了,關閉 channel 了。加上關閉 channel 程式碼後運行程式:
read ch: 0 , ok: true read ch: 1 , ok: true read ch: 2 , ok: true read ch: 3 , ok: true read ch: 0 , ok: false end
程式正常輸出結果。
對於沒有緩衝區的 channel (unbuffered channel) 容易產生死鎖的幾個程式碼片段分析,總結下:
- channel 要用 make 進行初始化操作
- 讀取和寫入要配對出現,並且不能在同一個 goroutine 里
- 一定先用 go 起一個協程執行讀取或寫入操作
- 多次寫入數據,for 讀取數據時,寫入者注意關閉 channel(程式碼片段5)
-
已初始化,有緩衝區的 channel
// 程式碼片段1 func main() { ch := make(chan int, 1) val, ok := <-ch }
程式碼片段1:有緩衝channel,先讀數據,這裡會一直阻塞,產生死鎖。
// 程式碼片段2 func main() { ch := make(chan int, 1) ch <- 10 }
程式碼片段2:同程式碼片段1,有緩衝channel,只有寫沒有讀,也會阻塞,產生死鎖。
// 程式碼片段3 func main() { ch := make(chan int, 1) ch <- 10 val, ok := <-ch if ok { fmt.Println(val, ok) } }
程式碼片段3:有緩衝的channel,有讀有寫,正常的輸出結果。
有緩衝區的channel總結:
如果 channel 滿了,發送者會阻塞
如果 channle 空了,接收者會阻塞
如果在同一個 goroutine 里,寫數據操作一定在讀數據操作前
參考
- //go.dev/tour/concurrency
- //go.dev/ref/spec#Channel_types
- //go.dev/ref/spec#Send_statements
- //go.dev/ref/spec#Receive_operator
- //go.dev/ref/spec#Close
- //go.dev/doc/effective_go#channels
- //go.dev/ref/spec#Select_statements
- //gobyexample.com/
- Concurrency is not parallelism – The Go Programming Language