GoLang?channel關(guān)閉狀態(tài)相關(guān)操作詳解
關(guān)于 channel 的使用,有幾點(diǎn)不方便的地方:
1.在不改變 channel 自身狀態(tài)的情況下,無(wú)法獲知一個(gè) channel 是否關(guān)閉。
2.關(guān)閉一個(gè) closed channel 會(huì)導(dǎo)致 panic。所以,如果關(guān)閉 channel 的一方在不知道 channel 是否處于關(guān)閉狀態(tài)時(shí)就去貿(mào)然關(guān)閉 channel 是很危險(xiǎn)的事情。
3.向一個(gè) closed channel 發(fā)送數(shù)據(jù)會(huì)導(dǎo)致 panic。所以,如果向 channel 發(fā)送數(shù)據(jù)的一方不知道 channel 是否處于關(guān)閉狀態(tài)時(shí)就去貿(mào)然向 channel 發(fā)送數(shù)據(jù)是很危險(xiǎn)的事情。
一個(gè)比較粗糙的檢查 channel 是否關(guān)閉的函數(shù):
func IsClosed(ch <-chan T) bool {
select {
case <-ch:
return true
default:
}
return false
}
func main() {
c := make(chan T)
fmt.Println(IsClosed(c)) // false
close(c)
fmt.Println(IsClosed(c)) // true
}看一下代碼,其實(shí)存在很多問(wèn)題。首先,IsClosed 函數(shù)是一個(gè)有副作用的函數(shù)。每調(diào)用一次,都會(huì)讀出 channel 里的一個(gè)元素,改變了 channel 的狀態(tài)。這不是一個(gè)好的函數(shù),干活就干活,還順手牽羊!
其次,IsClosed 函數(shù)返回的結(jié)果僅代表調(diào)用那個(gè)瞬間,并不能保證調(diào)用之后會(huì)不會(huì)有其他 goroutine 對(duì)它進(jìn)行了一些操作,改變了它的這種狀態(tài)。例如,IsClosed 函數(shù)返回 true,但這時(shí)有另一個(gè) goroutine 關(guān)閉了 channel,而你還拿著這個(gè)過(guò)時(shí)的 “channel 未關(guān)閉”的信息,向其發(fā)送數(shù)據(jù),就會(huì)導(dǎo)致 panic 的發(fā)生。當(dāng)然,一個(gè) channel 不會(huì)被重復(fù)關(guān)閉兩次,如果 IsClosed 函數(shù)返回的結(jié)果是 true,說(shuō)明 channel 是真的關(guān)閉了。
有一條廣泛流傳的關(guān)閉 channel 的原則:
don’t close a channel from the receiver side and don’t close a channel if the channel has multiple concurrent senders.
不要從一個(gè) receiver 側(cè)關(guān)閉 channel,也不要在有多個(gè) sender 時(shí),關(guān)閉 channel。
比較好理解,向 channel 發(fā)送元素的就是 sender,因此 sender 可以決定何時(shí)不發(fā)送數(shù)據(jù),并且關(guān)閉 channel。但是如果有多個(gè) sender,某個(gè) sender 同樣沒(méi)法確定其他 sender 的情況,這時(shí)也不能貿(mào)然關(guān)閉 channel。
但是上面所說(shuō)的并不是最本質(zhì)的,最本質(zhì)的原則就只有一條:
don’t close (or send values to) closed channels.
有兩個(gè)不那么優(yōu)雅地關(guān)閉 channel 的方法:
1.使用 defer-recover 機(jī)制,放心大膽地關(guān)閉 channel 或者向 channel 發(fā)送數(shù)據(jù)。即使發(fā)生了 panic,有 defer-recover 在兜底。
2.使用 sync.Once 來(lái)保證只關(guān)閉一次。
那到底應(yīng)該如何優(yōu)雅地關(guān)閉 channel?
根據(jù) sender 和 receiver 的個(gè)數(shù),分下面幾種情況:
- 一個(gè) sender,一個(gè) receiver
- 一個(gè) sender, M 個(gè) receiver
- N 個(gè) sender,一個(gè) reciver
- N 個(gè) sender, M 個(gè) receiver
對(duì)于 1,2,只有一個(gè) sender 的情況就不用說(shuō)了,直接從 sender 端關(guān)閉就好了,沒(méi)有問(wèn)題。重點(diǎn)關(guān)注第 3,4 種情況。
第 3 種情形下,優(yōu)雅關(guān)閉 channel 的方法是:the only receiver says “please stop sending more” by closing an additional signal channel。
解決方案就是增加一個(gè)傳遞關(guān)閉信號(hào)的 channel,receiver 通過(guò)信號(hào) channel 下達(dá)關(guān)閉數(shù)據(jù) channel 指令。senders 監(jiān)聽(tīng)到關(guān)閉信號(hào)后,停止發(fā)送數(shù)據(jù)。代碼如下:
func main() {
rand.Seed(time.Now().UnixNano())
const Max = 100000
const NumSenders = 1000
dataCh := make(chan int, 100)
stopCh := make(chan struct{})
// senders
for i := 0; i < NumSenders; i++ {
go func() {
for {
select {
case <- stopCh:
return
case dataCh <- rand.Intn(Max):
}
}
}()
}
// the receiver
go func() {
for value := range dataCh {
if value == Max-1 {
fmt.Println("send stop signal to senders.")
close(stopCh)
return
}
fmt.Println(value)
}
}()
select {
case <- time.After(time.Hour):
}
}這里的 stopCh 就是信號(hào) channel,它本身只有一個(gè) sender,因此可以直接關(guān)閉它。senders 收到了關(guān)閉信號(hào)后,select 分支 “case <- stopCh” 被選中,退出函數(shù),不再發(fā)送數(shù)據(jù)。
需要說(shuō)明的是,上面的代碼并沒(méi)有明確關(guān)閉 dataCh。在 Go 語(yǔ)言中,對(duì)于一個(gè) channel,如果最終沒(méi)有任何 goroutine 引用它,不管 channel 有沒(méi)有被關(guān)閉,最終都會(huì)被 gc 回收。所以,在這種情形下,所謂的優(yōu)雅地關(guān)閉 channel 就是不關(guān)閉 channel,讓 gc 代勞。
最后一種情況,優(yōu)雅關(guān)閉 channel 的方法是:any one of them says “let’s end the game” by notifying a moderator to close an additional signal channel。
和第 3 種情況不同,這里有 M 個(gè) receiver,如果直接還是采取第 3 種解決方案,由 receiver 直接關(guān)閉 stopCh 的話,就會(huì)重復(fù)關(guān)閉一個(gè) channel,導(dǎo)致 panic。因此需要增加一個(gè)中間人,M 個(gè) receiver 都向它發(fā)送關(guān)閉 dataCh 的“請(qǐng)求”,中間人收到第一個(gè)請(qǐng)求后,就會(huì)直接下達(dá)關(guān)閉 dataCh 的指令(通過(guò)關(guān)閉 stopCh,這時(shí)就不會(huì)發(fā)生重復(fù)關(guān)閉的情況,因?yàn)?stopCh 的發(fā)送方只有中間人一個(gè))。另外,這里的 N 個(gè) sender 也可以向中間人發(fā)送關(guān)閉 dataCh 的請(qǐng)求。
func main() {
rand.Seed(time.Now().UnixNano())
const Max = 100000
const NumReceivers = 10
const NumSenders = 1000
dataCh := make(chan int, 100)
stopCh := make(chan struct{})
// It must be a buffered channel.
toStop := make(chan string, 1)
var stoppedBy string
// moderator
go func() {
stoppedBy = <-toStop
close(stopCh)
}()
// senders
for i := 0; i < NumSenders; i++ {
go func(id string) {
for {
value := rand.Intn(Max)
if value == 0 {
select {
case toStop <- "sender#" + id:
default:
}
return
}
select {
case <- stopCh:
return
case dataCh <- value:
}
}
}(strconv.Itoa(i))
}
// receivers
for i := 0; i < NumReceivers; i++ {
go func(id string) {
for {
select {
case <- stopCh:
return
case value := <-dataCh:
if value == Max-1 {
select {
case toStop <- "receiver#" + id:
default:
}
return
}
fmt.Println(value)
}
}
}(strconv.Itoa(i))
}
select {
case <- time.After(time.Hour):
}
}代碼里 toStop 就是中間人的角色,使用它來(lái)接收 senders 和 receivers 發(fā)送過(guò)來(lái)的關(guān)閉 dataCh 請(qǐng)求。
這里將 toStop 聲明成了一個(gè) 緩沖型的 channel。假設(shè) toStop 聲明的是一個(gè)非緩沖型的 channel,那么第一個(gè)發(fā)送的關(guān)閉 dataCh 請(qǐng)求可能會(huì)丟失。因?yàn)闊o(wú)論是 sender 還是 receiver 都是通過(guò) select 語(yǔ)句來(lái)發(fā)送請(qǐng)求,如果中間人所在的 goroutine 沒(méi)有準(zhǔn)備好,那 select 語(yǔ)句就不會(huì)選中,直接走 default 選項(xiàng),什么也不做。這樣,第一個(gè)關(guān)閉 dataCh 的請(qǐng)求就會(huì)丟失。
如果,我們把 toStop 的容量聲明成 Num(senders) + Num(receivers),那發(fā)送 dataCh 請(qǐng)求的部分可以改成更簡(jiǎn)潔的形式:
...
toStop := make(chan string, NumReceivers + NumSenders)
...
value := rand.Intn(Max)
if value == 0 {
toStop <- "sender#" + id
return
}
...
if value == Max-1 {
toStop <- "receiver#" + id
return
}
...直接向 toStop 發(fā)送請(qǐng)求,因?yàn)?toStop 容量足夠大,所以不用擔(dān)心阻塞,自然也就不用 select 語(yǔ)句再加一個(gè) default case 來(lái)避免阻塞。
可以看到,這里同樣沒(méi)有真正關(guān)閉 dataCh,原樣同第 3 種情況。
到此這篇關(guān)于GoLang channel關(guān)閉狀態(tài)相關(guān)操作詳解的文章就介紹到這了,更多相關(guān)Go channel內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Golang中實(shí)現(xiàn)類似類與繼承的方法(示例代碼)
這篇文章主要介紹了Golang中實(shí)現(xiàn)類似類與繼承的方法,Go語(yǔ)言中通過(guò)方法接受者的類型來(lái)決定方法的歸屬和繼承關(guān)系,本文通過(guò)示例代碼講解的非常詳細(xì),需要的朋友可以參考下2024-04-04
Golang項(xiàng)目在github創(chuàng)建release后自動(dòng)生成二進(jìn)制文件的方法
這篇文章主要介紹了Golang項(xiàng)目在github創(chuàng)建release后如何自動(dòng)生成二進(jìn)制文件,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-03-03
Golang中空的切片轉(zhuǎn)化成 JSON 后變?yōu)?nbsp;null 問(wèn)題的解決方案
在 Golang 中,經(jīng)常需要將其他類型(例如 slice、map、struct 等類型)的數(shù)據(jù)轉(zhuǎn)化為 JSON 格式,有時(shí)候轉(zhuǎn)化的結(jié)果并不是預(yù)期中的,例如將一個(gè)空的切片轉(zhuǎn)化為 JSON 時(shí),會(huì)變成"null",所以本文將給大家介紹一下解決方法,需要的朋友可以參考下2023-09-09
Golang JSON的進(jìn)階用法實(shí)例講解
這篇文章主要給大家介紹了關(guān)于Golang JSON進(jìn)階用法的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家學(xué)習(xí)或者使用golang具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2018-09-09
Go語(yǔ)言TCP從原理到代碼實(shí)現(xiàn)詳解
這篇文章主要為大家介紹了Go語(yǔ)言TCP從原理到代碼實(shí)現(xiàn)詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-08-08
go?sync?Waitgroup數(shù)據(jù)結(jié)構(gòu)實(shí)現(xiàn)基本操作詳解
這篇文章主要為大家介紹了go?sync?Waitgroup數(shù)據(jù)結(jié)構(gòu)實(shí)現(xiàn)基本操作詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-01-01

