Go 管道關閉引發的探索

前言

在日常開發中, 經常會使用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()
	}
}

很快, 你就會看到報錯了:

image-20230219142712001

報錯的原因也很簡單, 就是因爲向已關閉的管道中寫數據.

我們如何能實現安全的管道關閉操作呢?

注意, 本次不討論重複調用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變量的訪問與賦值通過加鎖來實現原子操作, 那麼我們直接讓CloseSend函數全體加鎖, 將其強行改爲串行, 不就解決了嘛?

開心的告訴你, 沒錯, 這樣確實解決了.而且, 加鎖的粒度不能再小了,

但是, 不得不遺憾的告訴你, 這麼寫只能說看起來對.

問題: 因爲向管道寫數據是阻塞的, 當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
	}
}

等等吧, 其實有很多方式來實現, 在這裏就不一一贅述了.

總結

回過頭來再想.

  1. 爲什麼在官方的設計中, 管道不能重複關閉?
  2. 爲什麼向已關閉的管道發送數據會panic, 卻可以從已關閉的管道讀取數據?

從官方 API 的設計中, 我們可以猜到其希望調用方做的事情:

  1. 明確的知道自己什麼時候關閉管道, 且僅有一個人來關閉
  2. 發送方明確的知道管道是否已經關閉, 所以在管道關閉後不會再寫數據
  3. 接收方不知道管道的關閉情況, 因此需要通過返回值來判斷

而我們關閉管道的目的是什麼呢? 無非是:

  1. 通知接收方, 管道已經關閉, 處理完管道中的餘量就可以退出了
  2. 通知發送方, 不要再發送數據量

那麼, 我們就目的而言來分析:

  1. 通知接收方, 這個在官方的設計中就已經支持了, 只要保證管道不會重複close就行
  2. 通知發送方. 這裏我們分兩種情況來看:
    1. 後續沒有數據需要發送. 此時貌似也不需要通知哈.
    2. 還有數據沒有發送出去. 若還有數據的話, 不發出去是否會有問題? 是否應該等數據發完再關閉? 後續沒發出去的數據如何處理?

綜上, 我得出如下結論, 而這想必也是官方的意思吧:

  1. 管道的關閉應由發送方負責
  2. 發送方需在確認所有數據都發出之後再關閉管道

那麼, 如果真的真的就是要在發送完之前關閉管道怎麼辦呢?

  • 自查是否是設計問題, 導致管道的關閉者無法獲得數據的發送信息
  • 若流程實在無法更改, 推薦使用一個第三者(管道或鎖)來將關閉狀態通知給發送方和接收方, 而不關閉使用中的管道
    • 因爲管道本身就已經加鎖了, 如果再通過加鎖來實現的話無疑會造成額外的性能損耗. 甚至於我認爲recover都要被加鎖更好.
    • 當然了, 如果能通過修改設計來保證的話就更好了

以上, 如果你有什麼其他意見, 煩請不吝賜教


原文地址: https://hujingnb.com/archives/888

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章