1. 程式人生 > >Golang併發模型:併發協程的優雅退出

Golang併發模型:併發協程的優雅退出

goroutine作為Golang併發的核心,我們不僅要關注它們的建立和管理,當然還要關注如何合理的退出這些協程,不(合理)退出不然可能會造成阻塞、panic、程式行為異常、資料結果不正確等問題。這篇文章介紹,如何合理的退出goroutine,減少軟體bug。

goroutine在退出方面,不像執行緒和程序,不能通過某種手段強制關閉它們,只能等待goroutine主動退出。但也無需為退出、關閉goroutine而煩惱,下面就介紹3種優雅退出goroutine的方法,只要採用這種最佳實踐去設計,基本上就可以確保goroutine退出上不會有問題,盡情享用。

1:使用for-range退出

for-range是使用頻率很高的結構,常用它來遍歷資料,range能夠感知channel的關閉,當channel被髮送資料的協程關閉時,range就會結束,接著退出for迴圈。

它在併發中的使用場景是:當協程只從1個channel讀取資料,然後進行處理,處理後協程退出。下面這個示例程式,當in通道被關閉時,協程可自動退出。

go func(in <-chan int) {
    // Using for-range to exit goroutine
    // range has the ability to detect the close/end of a channel
for x := range in { fmt.Printf("Process %d\n", x) } }(inCh)

2:使用,ok退出

for-select也是使用頻率很高的結構,select提供了多路複用的能力,所以for-select可以讓函式具有持續多路處理多個channel的能力。但select沒有感知channel的關閉,這引出了2個問題

  1. 繼續在關閉的通道上讀,會讀到通道傳輸資料型別的零值,如果是指標型別,讀到nil,繼續處理還會產生nil。
  2. 繼續在關閉的通道上寫,將會panic。

問題2可以這樣解決,通道只由傳送方關閉,接收方不可關閉,即某個寫通道只由使用該select的協程關閉,select中就不存在繼續在關閉的通道上寫資料的問題。

問題1可以使用,ok來檢測通道的關閉,使用情況有2種。

第一種:如果某個通道關閉後,需要退出協程,直接return即可。示例程式碼中,該協程需要從in通道讀資料,還需要定時列印已經處理的數量,有2件事要做,所有不能使用for-range,需要使用for-select,當in關閉時,ok=false,我們直接返回。

go func() {
	// in for-select using ok to exit goroutine
	for {
		select {
		case x, ok := <-in:
			if !ok {
				return
			}
			fmt.Printf("Process %d\n", x)
			processedCnt++
		case <-t.C:
			fmt.Printf("Working, processedCnt = %d\n", processedCnt)
		}
	}
}()

第二種:如果某個通道關閉了,不再處理該通道,而是繼續處理其他case,退出是等待所有的可讀通道關閉。我們需要使用select的一個特徵:select不會在nil的通道上進行等待。這種情況,把只讀通道設定為nil即可解決。

go func() {
	// in for-select using ok to exit goroutine
	for {
		select {
		case x, ok := <-in1:
			if !ok {
				in1 = nil
			}
			// Process
		case y, ok := <-in2:
			if !ok {
				in2 = nil
			}
			// Process
		case <-t.C:
			fmt.Printf("Working, processedCnt = %d\n", processedCnt)
		}

		// If both in channel are closed, goroutine exit
		if in1 == nil && in2 == nil {
			return
		}
	}
}()

3:使用退出通道退出

使用,ok來退出使用for-select協程,解決是當讀入資料的通道關閉時,沒資料讀時程式的正常結束。想想下面這2種場景,,ok還能適用嗎?

  1. 接收的協程要退出了,如果它直接退出,不告知傳送協程,傳送協程將阻塞。
  2. 啟動了一個工作協程處理資料,如何通知它退出?

使用一個專門的通道,傳送退出的訊號,可以解決這類問題。以第2個場景為例,協程入參包含一個停止通道stopCh,當stopCh被關閉,case <-stopCh會執行,直接返回即可。

當我啟動了100個worker時,只要main()執行關閉stopCh,每一個worker都會都到訊號,進而關閉。如果main()向stopCh傳送100個數據,這種就低效了。

func worker(stopCh <-chan struct{}) {
	go func() {
		defer fmt.Println("worker exit")
		// Using stop channel explicit exit
		for {
			select {
			case <-stopCh:
				fmt.Println("Recv stop signal")
				return
			case <-t.C:
				fmt.Println("Working .")
			}
		}
	}()
	return
}

最佳實踐回顧

  1. 傳送協程主動關閉通道,接收協程不關閉通道。技巧:把接收方的通道入參宣告為只讀,如果接收協程關閉只讀協程,編譯時就會報錯。
  2. 協程處理1個通道,並且是讀時,協程優先使用for-range,因為range可以關閉通道的關閉自動退出協程。
  3. ,ok可以處理多個讀通道關閉,需要關閉當前使用for-select的協程。
  4. 顯式關閉通道stopCh可以處理主動通知協程退出的場景。

完整示例程式碼

本文所有程式碼都在倉庫,可檢視完整示例程式碼:https://github.com/Shitaibin/golang_goroutine_exit

併發系列文章推薦

  1. 如果這篇文章對你有幫助,不妨關注下我的Github,有文章會收到通知。
  2. 本文作者:大彬
  3. 如果喜歡本文,隨意轉載,但請保留此原文連結:http://lessisbetter.site/2018/12/02/golang-exit-goroutine-in-3-ways/
關注公眾號,獲取最新Golang文章。 一起學Golang-分享有料的Go語言技術