網站首頁 編程語言 正文
關于 channel 的使用,有幾點不方便的地方:
1.在不改變 channel 自身狀態的情況下,無法獲知一個 channel 是否關閉。
2.關閉一個 closed channel 會導致 panic。所以,如果關閉 channel 的一方在不知道 channel 是否處于關閉狀態時就去貿然關閉 channel 是很危險的事情。
3.向一個 closed channel 發送數據會導致 panic。所以,如果向 channel 發送數據的一方不知道 channel 是否處于關閉狀態時就去貿然向 channel 發送數據是很危險的事情。
一個比較粗糙的檢查 channel 是否關閉的函數:
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 }
看一下代碼,其實存在很多問題。首先,IsClosed 函數是一個有副作用的函數。每調用一次,都會讀出 channel 里的一個元素,改變了 channel 的狀態。這不是一個好的函數,干活就干活,還順手牽羊!
其次,IsClosed 函數返回的結果僅代表調用那個瞬間,并不能保證調用之后會不會有其他 goroutine 對它進行了一些操作,改變了它的這種狀態。例如,IsClosed 函數返回 true,但這時有另一個 goroutine 關閉了 channel,而你還拿著這個過時的 “channel 未關閉”的信息,向其發送數據,就會導致 panic 的發生。當然,一個 channel 不會被重復關閉兩次,如果 IsClosed 函數返回的結果是 true,說明 channel 是真的關閉了。
有一條廣泛流傳的關閉 channel 的原則:
don’t close a channel from the receiver side and don’t close a channel if the channel has multiple concurrent senders.
不要從一個 receiver 側關閉 channel,也不要在有多個 sender 時,關閉 channel。
比較好理解,向 channel 發送元素的就是 sender,因此 sender 可以決定何時不發送數據,并且關閉 channel。但是如果有多個 sender,某個 sender 同樣沒法確定其他 sender 的情況,這時也不能貿然關閉 channel。
但是上面所說的并不是最本質的,最本質的原則就只有一條:
don’t close (or send values to) closed channels.
有兩個不那么優雅地關閉 channel 的方法:
1.使用 defer-recover 機制,放心大膽地關閉 channel 或者向 channel 發送數據。即使發生了 panic,有 defer-recover 在兜底。
2.使用 sync.Once 來保證只關閉一次。
那到底應該如何優雅地關閉 channel?
根據 sender 和 receiver 的個數,分下面幾種情況:
- 一個 sender,一個 receiver
- 一個 sender, M 個 receiver
- N 個 sender,一個 reciver
- N 個 sender, M 個 receiver
對于 1,2,只有一個 sender 的情況就不用說了,直接從 sender 端關閉就好了,沒有問題。重點關注第 3,4 種情況。
第 3 種情形下,優雅關閉 channel 的方法是:the only receiver says “please stop sending more” by closing an additional signal channel。
解決方案就是增加一個傳遞關閉信號的 channel,receiver 通過信號 channel 下達關閉數據 channel 指令。senders 監聽到關閉信號后,停止發送數據。代碼如下:
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 就是信號 channel,它本身只有一個 sender,因此可以直接關閉它。senders 收到了關閉信號后,select 分支 “case <- stopCh” 被選中,退出函數,不再發送數據。
需要說明的是,上面的代碼并沒有明確關閉 dataCh。在 Go 語言中,對于一個 channel,如果最終沒有任何 goroutine 引用它,不管 channel 有沒有被關閉,最終都會被 gc 回收。所以,在這種情形下,所謂的優雅地關閉 channel 就是不關閉 channel,讓 gc 代勞。
最后一種情況,優雅關閉 channel 的方法是:any one of them says “let’s end the game” by notifying a moderator to close an additional signal channel。
和第 3 種情況不同,這里有 M 個 receiver,如果直接還是采取第 3 種解決方案,由 receiver 直接關閉 stopCh 的話,就會重復關閉一個 channel,導致 panic。因此需要增加一個中間人,M 個 receiver 都向它發送關閉 dataCh 的“請求”,中間人收到第一個請求后,就會直接下達關閉 dataCh 的指令(通過關閉 stopCh,這時就不會發生重復關閉的情況,因為 stopCh 的發送方只有中間人一個)。另外,這里的 N 個 sender 也可以向中間人發送關閉 dataCh 的請求。
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 就是中間人的角色,使用它來接收 senders 和 receivers 發送過來的關閉 dataCh 請求。
這里將 toStop 聲明成了一個 緩沖型的 channel。假設 toStop 聲明的是一個非緩沖型的 channel,那么第一個發送的關閉 dataCh 請求可能會丟失。因為無論是 sender 還是 receiver 都是通過 select 語句來發送請求,如果中間人所在的 goroutine 沒有準備好,那 select 語句就不會選中,直接走 default 選項,什么也不做。這樣,第一個關閉 dataCh 的請求就會丟失。
如果,我們把 toStop 的容量聲明成 Num(senders) + Num(receivers),那發送 dataCh 請求的部分可以改成更簡潔的形式:
... 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 發送請求,因為 toStop 容量足夠大,所以不用擔心阻塞,自然也就不用 select 語句再加一個 default case 來避免阻塞。
可以看到,這里同樣沒有真正關閉 dataCh,原樣同第 3 種情況。
原文鏈接:https://blog.csdn.net/qq_53267860/article/details/126918034
相關推薦
- 2022-08-25 .net?core中高效的動態內存管理方案_實用技巧
- 2022-07-13 Android?Studio實現簡單繪圖板_Android
- 2023-06-13 Python?Beautiful?Soup模塊使用教程詳解_python
- 2022-11-29 C#中泛型類和擴展方法如何使用_C#教程
- 2022-09-10 ELK收集Tomcat日志的實現_Tomcat
- 2022-10-17 Python?Django源碼運行過程解析_python
- 2022-10-07 numpy拼接矩陣的實現_python
- 2022-03-27 mongodb啟動方法小結_MongoDB
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支