前言
在日常開發中, 經常會使用chan
來進行協程之間的通信. 對chan
的操作也無外乎讀寫關. 而本次, 就是從chan
的關閉而來.
假設我們對外提供的方法如下:
type Chan struct {
ch chan int
}
func (c *Chan) Close() {
close(c.ch)
}
func (c *Chan) Send(v int) {
c.ch <- v
}
那麼, 簡單寫段邏輯測試一下:
func main() {
for {
c := &Chan{ch: make(chan int, 100)}
w := sync.WaitGroup{}
w.Add(2)
go func() {
defer w.Done()
c.Send(1)
}()
go func() {
defer w.Done()
c.Close()
}()
w.Wait()
}
}
很快, 你就會看到報錯了:
報錯的原因也很簡單, 就是因爲向已關閉的管道中寫數據.
我們如何能實現安全的管道關閉操作呢?
注意, 本次不討論重複調用Close
方法的情況, 這種只要加鎖保證單次執行就可以了, 情況簡單.
如何關閉管道
1.漏洞百出版
有的小夥伴想到了, 加個狀態判斷不就行了, 管道關閉之後就不再往裏寫咯. 並很快給出了代碼:
type Chan struct {
ch chan int
isClosed bool
}
func (c *Chan) Close() {
c.isClosed = true
close(c.ch)
}
func (c *Chan) Send(v int) bool {
if c.isClosed {
return false
}
c.ch <- v
return true
}
但是, 我相信在運行之後就不會這麼想了.
問題: 如果在發送時的 isClosed
判斷與chan
通信之間發生了管道的關閉, 還是會出錯.
2. 粗糙的正確加鎖版
既然發生錯誤的原因是操作的原則性, 那很自然的就會想到加鎖.
type Chan struct {
ch chan int
isClosed bool
lock sync.Mutex
}
func (c *Chan) Close() {
c.lock.Lock()
defer c.lock.Unlock()
c.isClosed = true
close(c.ch)
}
func (c *Chan) Send(v int) bool {
c.lock.Lock()
defer c.lock.Unlock()
if c.isClosed {
return false
}
c.ch <- v
return true
}
將isClosed
變量的訪問與賦值通過加鎖來實現原子操作, 那麼我們直接讓Close
和Send
函數全體加鎖, 將其強行改爲串行, 不就解決了嘛?
開心的告訴你, 沒錯, 這樣確實解決了.而且, 加鎖的粒度不能再小了,
但是, 不得不遺憾的告訴你, 這麼寫只能說看起來對.
問題: 因爲向管道寫數據是阻塞的, 當chan
滿了再寫數據, 就會阻塞在這裏, 持有的鎖就不會釋放. 導致關閉函數一直無法執行.
有的小機靈鬼想到了, 使用select-case
將阻塞寫改爲非阻塞寫不就行了嘛? 確實可以, 但這樣無疑會增加調用者的成本.
3. 小機靈版本
有的小機靈想到了, 我再Send
的時候把panic
抓住處理掉不就行了麼.
type Chan struct {
ch chan int
}
func (c *Chan) Close() {
close(c.ch)
}
func (c *Chan) Send(v int) (ret bool) {
defer func() {
if r := recover(); r != nil {
ret = false
}
}()
c.ch <- v
return true
}
開心的告訴你, 確實可以. 而且不存在鎖的競爭問題, 很好.
但是, 在Go
的哲學中, 流程中的異常是通過error
返回, 通過捕捉panic
來實現總覺得怪怪的.
4. Sender
關閉
既然總會遇到風險, 那麼我在Send
漢中中進行close
可以麼?
type Chan struct {
ch chan int
needClose bool
once sync.Once
}
func (c *Chan) Close() {
c.needClose = true
}
func (c *Chan) Send(v int) (ret bool) {
if c.needClose {
c.once.Do(func() {
close(c.ch)
})
return false
}
c.ch <- v
return true
}
這樣確實也是可以的, 但是別高興的太早.
問題: 如果在Close
之後沒有調用Send
方法, 那麼此chan
就不會關閉, 也就無法通知到接收方了. 因此, 十分不推薦.
包括使用額外的chan
來實現, 也不推薦, 因爲沒有將chan
關閉, 無法通知接收方(如下):
type Chan struct {
ch chan int
done chan int
}
func (c *Chan) Close() {
close(c.done)
}
func (c *Chan) Send(v int) (ret bool) {
select {
case <-c.done:
return false
case c.ch <- v:
return true
}
}
等等吧, 其實有很多方式來實現, 在這裏就不一一贅述了.
總結
回過頭來再想.
- 爲什麼在官方的設計中, 管道不能重複關閉?
- 爲什麼向已關閉的管道發送數據會
panic
, 卻可以從已關閉的管道讀取數據?
從官方 API 的設計中, 我們可以猜到其希望調用方做的事情:
- 明確的知道自己什麼時候關閉管道, 且僅有一個人來關閉
- 發送方明確的知道管道是否已經關閉, 所以在管道關閉後不會再寫數據
- 接收方不知道管道的關閉情況, 因此需要通過返回值來判斷
而我們關閉管道的目的是什麼呢? 無非是:
- 通知接收方, 管道已經關閉, 處理完管道中的餘量就可以退出了
- 通知發送方, 不要再發送數據量
那麼, 我們就目的而言來分析:
- 通知接收方, 這個在官方的設計中就已經支持了, 只要保證管道不會重複
close
就行 - 通知發送方. 這裏我們分兩種情況來看:
- 後續沒有數據需要發送. 此時貌似也不需要通知哈.
- 還有數據沒有發送出去. 若還有數據的話, 不發出去是否會有問題? 是否應該等數據發完再關閉? 後續沒發出去的數據如何處理?
綜上, 我得出如下結論, 而這想必也是官方的意思吧:
- 管道的關閉應由發送方負責
- 發送方需在確認所有數據都發出之後再關閉管道
那麼, 如果真的真的就是要在發送完之前關閉管道怎麼辦呢?
- 自查是否是設計問題, 導致管道的關閉者無法獲得數據的發送信息
- 若流程實在無法更改, 推薦使用一個第三者(管道或鎖)來將關閉狀態通知給發送方和接收方, 而不關閉使用中的管道
- 因爲管道本身就已經加鎖了, 如果再通過加鎖來實現的話無疑會造成額外的性能損耗. 甚至於我認爲
recover
都要被加鎖更好. - 當然了, 如果能通過修改設計來保證的話就更好了
- 因爲管道本身就已經加鎖了, 如果再通過加鎖來實現的話無疑會造成額外的性能損耗. 甚至於我認爲
以上, 如果你有什麼其他意見, 煩請不吝賜教