Golang併發模型:合理退出併發協程

goroutine做爲Golang併發的核心,咱們不只要關注它們的建立和管理,固然還要關注如何合理的退出這些協程,不(合理)退出否則可能會形成阻塞、panic、程序行爲異常、數據結果不正確等問題。這篇文章介紹,如何合理的退出goroutine,減小軟件bug。git

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

1:使用for-range退出

for-range是使用頻率很高的結構,經常使用它來遍歷數據,range可以感知channel的關閉,當channel被髮送數據的協程關閉時,range就會結束,接着退出for循環。golang

它在併發中的使用場景是:當協程只從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個問題less

  1. 繼續在關閉的通道上讀,會讀到通道傳輸數據類型的零值,若是是指針類型,讀到nil,繼續處理還會產生nil。
  2. 繼續在關閉的通道上寫,將會panic。

問題2能夠這樣解決,通道只由發送方關閉,接收方不可關閉,即某個寫通道只由使用該select的協程關閉,select中就不存在繼續在關閉的通道上寫數據的問題。函數

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

第一種:若是某個通道關閉後,須要退出協程,直接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/...

併發系列文章推薦

  1. 若是這篇文章對你有幫助,請點個贊/喜歡,鼓勵我持續分享,感謝。
  2. 個人文章列表,點此可查看
  3. 若是喜歡本文,隨意轉載,但請保留此原文連接

一塊兒學Golang-分享有料的Go語言技術

相關文章
相關標籤/搜索