golang?中?channel?的詳細使用、使用注意事項及死鎖問題解析
什么是 channel 管道
它是一個數(shù)據(jù)管道,可以往里面寫數(shù)據(jù),從里面讀數(shù)據(jù)。
channel 是 goroutine 之間數(shù)據(jù)通信橋梁,而且是線程安全的。
channel 遵循先進先出原則。
寫入,讀出數(shù)據(jù)都會加鎖。
channel 可以分為 3 種類型:
- 只讀 channel,單向 channel
- 只寫 channel,單向 channel
- 可讀可寫 channel
channel 還可按是否帶有緩沖區(qū)分為:
帶緩沖區(qū)的 channel,定義了緩沖區(qū)大小,可以存儲多個數(shù)據(jù)
不帶緩沖區(qū)的 channel,只能存一個數(shù)據(jù),并且只有當該數(shù)據(jù)被取出才能存下一個數(shù)據(jù)
channel 的基本使用
定義和聲明
// 只讀 channel var readOnlyChan <-chan int // channel 的類型為 int // 只寫 channel var writeOnlyChan chan<- int // 可讀可寫 var ch chan int // 或者使用 make 直接初始化 readOnlyChan1 := make(<-chan int, 2) // 只讀且?guī)Ь彺鎱^(qū)的 channel readOnlyChan2 := make(<-chan int) // 只讀且不帶緩存區(qū) channel writeOnlyChan3 := make(chan<- int, 4) // 只寫且?guī)Ь彺鎱^(qū) channel writeOnlyChan4 := make(chan<- int) // 只寫且不帶緩存區(qū) channel ch := make(chan int, 10) // 可讀可寫且?guī)Ь彺鎱^(qū) ch <- 20 // 寫數(shù)據(jù) i := <-ch // 讀數(shù)據(jù) i, ok := <-ch // 還可以判斷讀取的數(shù)據(jù)
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,它返回的值是一個內(nèi)存地址 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()函數(shù)里初始化的單向channel,修改為可讀可寫channel,再運行
chan_uni2.go
package main import "fmt" func main() { // 把上面代碼main()函數(shù)初始化的單向 channel 修改為可讀可寫的 channel ch := make(chan int) go testData(ch) fmt.Println(<-ch) } func testData(ch chan<- int) { ch <- 10 } // 運行輸出: // 10 // 沒有報錯,可以正常輸出結果
帶緩沖和不帶緩沖的 channel
不帶緩沖區(qū) 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) { 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
帶緩沖區(qū) 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,讀到數(shù)據(jù),且管道沒有關閉
- ok 為 false,管道已關閉,沒有數(shù)據(jù)可讀
讀已經(jīng)關閉的 channel 會讀到零值,如果不確定 channel 是否關閉,可以用這種方法來檢測。
range and close
range 可以遍歷數(shù)組,map,字符串,channel等。
一個發(fā)送者可以關閉 channel,表明沒有任何數(shù)據(jù)發(fā)送給這個 channel 了。接收者也可以測試channel是否關閉,通過 v, ok := <-ch
表達式中的 ok 值來判斷 channel 是否關閉。上一節(jié)已經(jīng)說明 ok 為 false 時,表示 channel 沒有接收任何數(shù)據(jù),它已經(jīng)關閉了。
注意:僅僅只能是發(fā)送者關閉一個 channel,而不能是接收者。給已經(jīng)關閉的 channel 發(fā)送數(shù)據(jù)會導致 panic。
Note: channels 不是文件,你通常不需要關閉他們。那什么時候需要關閉?當要告訴接收者沒有值發(fā)送給 channel 了,這時就需要了。
比如終止 range 循環(huán)。
當 for range 遍歷 channel 時,如果發(fā)送者沒有關閉 channel 或在 range 之后關閉,都會導致 deadlock(死鎖)。
下面是一個會產(chǎn)生死鎖的例子:
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已經(jīng)”通知“不到range了,會觸發(fā)死鎖。 // 不管這里是否關閉channel,都會報死鎖,close(ch)的位置就不對。 // 且關閉channel的操作者也錯了,只能是發(fā)送者關閉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
發(fā)送者關閉 channel 時,for range 循環(huán)自動退出。
for 讀取channel
用 for 來不停循環(huán)讀取 channel 里的數(shù)據(jù)。
把上面的 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,沒有數(shù)據(jù)可讀 break // 跳出循環(huán) } 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" // https://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的數(shù)據(jù)傳輸管道
package main import "fmt" // https://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
沒有緩沖區(qū)的 channel 可以作為同步數(shù)據(jù)的管道,起到同步數(shù)據(jù)的作用。
對沒有緩沖區(qū)的 channel 操作時,發(fā)送的 goroutine 和接收的 goroutine 需要同時準備好,也就是發(fā)送和接收需要一一配對,才能完成發(fā)送和接收的操作。
如果兩方的 goroutine 沒有同時準備好,channel 會導致先執(zhí)行發(fā)送或接收的 goroutine 阻塞等待。這就是沒有緩沖區(qū)的 channel 作為數(shù)據(jù)同步的作用。
gobyexample 中的一個例子:
package main import ( "fmt" "time" ) //https://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 協(xié)程里發(fā)送和接收數(shù)據(jù)??赡軐е耫eadlock死鎖。
3. 異步的channel
有緩沖區(qū)的 channel 可以作為異步的 channel 使用。
有緩沖區(qū)的 channel 也有操作注意事項:
如果 channel 中沒有值了,channel 為空了,那么接收者會被阻塞。
如果 channel 中的緩沖區(qū)滿了,那么發(fā)送者會被阻塞。
注意:有緩沖區(qū)的 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,表示這里的程序已經(jīng)執(zhí)行完了 }() ch <- 1 ch <- 2 ch <- 3 ch <- 4 ch <- 5 close(ch) // 用完關閉channel <-quitChan // 接到channel通知后解除阻塞,這也是channel的一種用法 }
4.channel 超時處理
channel 結合 time 實現(xiàn)超時處理。
當一個 channel 讀取數(shù)據(jù)超過一定時間還沒有數(shù)據(jù)到來時,可以得到超時通知,防止一直阻塞當前 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 讀寫關閉操作
1.讀:未初始化的channel,讀取里面的數(shù)據(jù)時,會造成死鎖deadlock
var ch chan int <-ch // 未初始化channel讀數(shù)據(jù)會死鎖
2.寫:未初始化的channel,往里面寫數(shù)據(jù)時,會造成死鎖deadlock
var ch chan int ch<- // 未初始化channel寫數(shù)據(jù)會死鎖
3.關閉:未初始化的channel,關閉該channel時,會panic
var ch chan int close(ch) // 關閉未初始化channel,觸發(fā)panic
已初始化的 channel 讀寫關閉操作
1. 已初始化,沒有緩沖區(qū)的channel
// 代碼片段1 func main() { ch := make(chan int) ch <- 4 }
代碼片段1:沒有緩沖channel,且只有寫入沒有讀取,會產(chǎn)生死鎖
// 代碼片段2 func main() { ch := make(chan int) val, ok := <-ch }
代碼片段2:沒有緩沖channel,且只有讀取沒有寫入,會產(chǎn)生死鎖
// 代碼片段3 func main() { ch := make(chan int) val, ok := <-ch if ok { fmt.Println(val) } ch <- 10 // 這里進行寫入。但是前面已經(jīng)產(chǎn)生死鎖了 }
代碼片段3:沒有緩沖channel,既有寫入也有讀出,但是在代碼 val, ok := <-c
處已經(jīng)產(chǎn)生死鎖了。下面代碼執(zhí)行不到。
// 代碼片段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 里寫入數(shù)據(jù)的代碼 ch <- 10
,這里寫入數(shù)據(jù)時就已經(jīng)產(chǎn)生死鎖了。把 ch<-10
和 go readChan(ch)
調(diào)換位置,程序就能正常運行,不會產(chǎn)生死鎖。
// 代碼片段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的數(shù)據(jù)不是一個。
思考一下,這個程序會產(chǎn)生死鎖嗎?10 秒時間思考下,先不要看下面。
也會產(chǎn)生死鎖,它會輸出完數(shù)據(jù)后,報錯 fatal error: all goroutines are asleep - deadlock!
。
為什么呢?這個程序片段,既有讀也有寫而且先開一個goroutine寫數(shù)據(jù),為什么會死鎖?
原因在于 main()
里的 for
循環(huán)??赡苣銜?,不是有 break
跳出 for
循環(huán)嗎?代碼是寫了,但是程序并沒有執(zhí)行到這里。
因為 for
會不停的循環(huán),而 val, ok := <-ch
, 這里 ok
值一直是 true,因為程序里并沒有哪里關閉 channel 啊。你們可以打印這個 ok
值看一看是不是一直是 true。當 for
循環(huán)把 channel 里的值讀取完了后,程序再次運行到 val, ok := <-ch
時,產(chǎn)生死鎖,因為 channel 里沒有數(shù)據(jù)了。
找到原因了,那解決辦法也很簡單,在 writeChan
函數(shù)里關閉 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
程序正常輸出結果。
對于沒有緩沖區(qū)的 channel (unbuffered channel) 容易產(chǎn)生死鎖的幾個代碼片段分析,總結下:
- channel 要用 make 進行初始化操作
- 讀取和寫入要配對出現(xiàn),并且不能在同一個 goroutine 里
- 一定先用 go 起一個協(xié)程執(zhí)行讀取或?qū)懭氩僮?/li>
- 多次寫入數(shù)據(jù),for 讀取數(shù)據(jù)時,寫入者注意關閉 channel(代碼片段5)
2. 已初始化,有緩沖區(qū)的 channel
// 代碼片段1 func main() { ch := make(chan int, 1) val, ok := <-ch }
代碼片段1:有緩沖channel,先讀數(shù)據(jù),這里會一直阻塞,產(chǎn)生死鎖。
// 代碼片段2 func main() { ch := make(chan int, 1) ch <- 10 }
代碼片段2:同代碼片段1,有緩沖channel,只有寫沒有讀,也會阻塞,產(chǎn)生死鎖。
// 代碼片段3 func main() { ch := make(chan int, 1) ch <- 10 val, ok := <-ch if ok { fmt.Println(val, ok) } }
代碼片段3:有緩沖的channel,有讀有寫,正常的輸出結果。
有緩沖區(qū)的channel總結:
- 如果 channel 滿了,發(fā)送者會阻塞
- 如果 channle 空了,接收者會阻塞
- 如果在同一個 goroutine 里,寫數(shù)據(jù)操作一定在讀數(shù)據(jù)操作前
參考
https://go.dev/tour/concurrency
https://go.dev/ref/spec#Channel_types
https://go.dev/ref/spec#Send_statements
https://go.dev/ref/spec#Receive_operator
https://go.dev/ref/spec#Close
https://go.dev/doc/effective_go#channels
https://go.dev/ref/spec#Select_statements
https://gobyexample.com/
Concurrency is not parallelism - The Go Programming Language
到此這篇關于golang 中 channel 的詳細使用、使用注意事項及死鎖分析的文章就介紹到這了,更多相關golang 中 channel 使用內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Golang中數(shù)據(jù)結構Queue的實現(xiàn)方法詳解
這篇文章主要給大家介紹了關于Golang中數(shù)據(jù)結構Queue的實現(xiàn)方法,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧。2017-09-09go內(nèi)存隊列l(wèi)ist VS slice實現(xiàn)方式對比分析
這篇文章主要為大家介紹了go內(nèi)存隊列l(wèi)ist VS slice實現(xiàn)方式對比分析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-08-08golang 函數(shù)以及函數(shù)和方法的詳解及區(qū)別
這篇文章主要介紹了golang 函數(shù)以及函數(shù)和方法的區(qū)別的相關資料,需要的朋友可以參考下2017-05-05