Goroutine
是Go語言最重要的機制,Goroutine
將複雜的需要非同步的IO呼叫抽象成同步呼叫,符合人類正常的順序思維,極大的簡化了IO程式設計的難度。如同執行緒一樣,對Goroutine
既要掌握基本的用法,更要很好的控制Goroutine
的退出機制。本文介紹一種Goroutine
的退出思路。
通常Goroutine
會因為兩種情況阻塞:
-
IO操作,比如對
Socket
的Read
。 -
channel
操作。對一個chan的讀寫都有可能阻塞Goroutine
。
對於情況1,只需要關閉對應的描述符,阻塞的Goroutine
自然會被喚醒。
重點討論情況2。併發程式設計,Goroutine
提供一種channel
機制,channel
類似管道,寫入者向裡面寫入資料,讀取者從中讀取資料。如果channel
裡面沒有資料,讀取者將阻塞,直到有資料;如果channel
裡面資料滿了,寫入者將因為無法繼續寫入資料而阻塞。
如果在整個應用程式的生命週期裡,writer和reader都表現為一個Goroutine
,始終都在工作,那麼如何在應用程式結束前,通知它們終止呢?在Go中,並不推薦像abort執行緒那樣,強行的終止Goroutine
。因此,抽象的說,必然需要保留一個入口,能夠跟writer或reader通訊,以告知它們終止。
我們先看reader。我們首先可以想到,利用close
函式關閉正在讀取的channel
,從而可以喚醒reader,並退出。但是考慮到close
並不能很好的處理writer(因為writer試圖寫入一個已經close的channel,將引發異常)。因此,我們需要設計一個額外的只讀channel
用於通知:
type routineSignal struct {
done <-chan struct{}
}
routineSignal
的例項,應當通過外部生成並傳遞給reader,例如:
func (r *reader)init(s *routineSignal) {
r.signal = s
}
在reader的迴圈中,就可以這麼寫:
func (r *reader)loop() {
for {
select {
case <-r.signal.done:
return
case <-r.queue:
....
}
}
}
當需要終止Goroutine
的時候只需要關閉這個額外的channel
:
close(signal.done)
看起來很完備了,這可以處理大部分的情況了。這樣做有個弊端,儘管,我們可以期望close
喚醒Goroutine
進而退出,但是並不能知道Goroutine
什麼時候完成退出,因為Goroutine
可能在退出前還有一些善後工作,這個時候我們需要sync.WaitGroup
。改造一下routineSignal
:
type routineSignal struct {
done chan struct{}
wg sync.WaitGroup
}
增加一個sync.WaitGroup的例項,在Goroutine
開始工作時,對wg加1,在Goroutine
退出前,對wg減1:
func (r *reader)loop() {
r.signal.wg.Add(1)
defer r.signal.wg.Done()
for {
select {
case <-r.signal.done:
return
case <-r.queue:
....
}
}
}
外部,只需要等待WaitGroup
返回即可:
close(signal.done)
signal.wg.Wait()
只要Wait()
返回就能斷定Goroutine
結束了。
推導一下,不難發現,對於writer也可以採用這種方法。於是,總結一下,我們建立了一個叫routineSignal
的結構,結構裡面包含一個chan
用來通知Goroutine
結束,包含一個WaitGroup
用於Goroutine
通知外部完成善後。這樣,通過這個結構的例項優雅的終止Goroutine
,而且還可以確保Goroutine
終止成功。