學會使用context取消goroutine執行的方法

Go語言裏每個併發的執行單元叫作goroutine,當一個用Go語言編寫的程序啓動時,其main函數在一個單獨的goroutine中運行。main函數返回時,全部的goroutine都會被直接打斷,程序退出。除此以外若是想經過編程的方法讓一個goroutine中斷其餘goroutine的執行,只能是經過在多個goroutine間經過context上下文對象同步取消信號的方式來實現。數據庫

這篇文章將介紹一些使用context對象同步信號取消中斷程序執行的經常使用模式和最佳實踐,從而讓咱們能構建更迅捷、健壯的應用程序。若是對context對象不太瞭解的同窗建議先仔細看看《Golang 併發編程之Context》瞭解一下基礎。編程

爲何須要取消功能

簡單來講,咱們須要取消功能來防止系統作一些沒必要要的工做。瀏覽器

考慮如下常見的場景:一個HTTP服務器查詢數據庫並將查詢到的數據做爲響應返回給客戶端:服務器

客戶端請求

若是一切正常,時序圖將以下所示:
請求處理時序圖併發

可是,若是客戶端在中途取消了請求會發生什麼?這種狀況能夠發生在,好比用戶在請求中途關閉了瀏覽器。若是不支持取消功能,HTTP服務器和數據庫會繼續工做,因爲客戶端已經關閉因此他們工做的成果也就被浪費了。這種狀況的時序圖以下所示:函數

不支持取消的處理時序圖

理想狀況下,若是咱們知道某個處理過程(在此示例中爲HTTP請求)已中止,則但願該過程的全部下游組件都中止運行:工具

支持取消的處理時序圖

使用context實現取消功能

如今咱們知道了應用程序爲何須要取消功能,接下來咱們開始探究在Go中如何實現它。由於「取消事件」與正在執行的操做高度相關,所以很天然地會將它與上下文捆綁在一塊兒。測試

取消功能須要從兩方面實現才能完成:google

  • 監聽取消事件
  • 發出取消事件

監聽取消事件

Go語言context標準庫的Context類型提供了一個Done()方法,該方法返回一個類型爲 <-chan struct{}channel。每次context收到取消事件後這個channel都會接收到一個struct{}類型的值。因此在Go語言裏監聽取消事件就是等待接收<-ctx.Done()spa

舉例來講,假設一個HTTP服務器須要花費兩秒鐘來處理一個請求。若是在處理完成以前請求被取消,咱們想讓程序能當即中斷再也不繼續執行下去:

func main() {
    // 建立一個監聽8000端口的服務器
    http.ListenAndServe(":8000", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        ctx := r.Context()
        // 輸出到STDOUT展現處理已經開始
        fmt.Fprint(os.Stdout, "processing request\n")
    // 經過select監聽多個channel
        select {
        case <-time.After(2 * time.Second):
      // 若是兩秒後接受到了一個消息後,意味請求已經處理完成
      // 咱們寫入"request processed"做爲響應
            w.Write([]byte("request processed"))
        case <-ctx.Done():

      // 若是處理完成前取消了,在STDERR中記錄請求被取消的消息
            fmt.Fprint(os.Stderr, "request cancelled\n")
        }
    }))
}

你能夠經過運行服務器並在瀏覽器中打開localhost:8000進行測試。若是你在2秒鐘前關閉瀏覽器,則應該在終端窗口上看到「request cancelled」字樣。

發出取消事件

若是你有一個能夠取消的操做,則必須經過context發出取消事件。能夠經過context包的WithCancel函數返回的取消函數來完成此操做(withCancel還會返回一個支持取消功能的上下文對象)。該函數不接受參數也不返回任何內容,當須要取消上下文時會調用該函數,發出取消事件。

考慮有兩個相互依賴的操做的狀況。在這裏,「依賴」是指若是其中一個失敗,那麼另外一個就沒有意義,而不是第二個操做依賴第一個操做的結果(那種狀況下,兩個操做不能並行)。在這種狀況下,若是咱們很早就知道其中一個操做失敗,那麼咱們就會但願能取消全部相關的操做。

func operation1(ctx context.Context) error {
  // 讓咱們假設這個操做會由於某種緣由失敗
  // 咱們使用time.Sleep來模擬一個資源密集型操做
    time.Sleep(100 * time.Millisecond)
    return errors.New("failed")
}

func operation2(ctx context.Context) {
  // 咱們使用在前面HTTP服務器例子裏使用過的類型模式
    select {
    case <-time.After(500 * time.Millisecond):
        fmt.Println("done")
    case <-ctx.Done():
        fmt.Println("halted operation2")
    }
}

func main() {
    // 新建一個上下文
    ctx := context.Background()
  // 在初始上下文的基礎上建立一個有取消功能的上下文
    ctx, cancel := context.WithCancel(ctx)
  // 在不一樣的goroutine中運行operation2
    go func() {
      operation2(ctx)
    }()
  
  err := operation1(ctx)
  // 若是這個操做返回錯誤,取消全部使用相同上下文的操做
    if err != nil {
        cancel()
    }
}

基於時間的取消

任何須要在請求的最大持續時間內維持SLA(服務水平協議)的應用程序,都應使用基於時間的取消。該API與前面的示例幾乎相同,但有一些補充:

// 這個上下文將會在3秒後被取消
// 若是須要在到期前就取消能夠像前面的例子那樣使用cancel函數
ctx, cancel := context.WithTimeout(ctx, 3*time.Second)

// 上下文將在2009-11-10 23:00:00被取消
ctx, cancel := context.WithDeadline(ctx, time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC))

例如,程序在對外部服務進行HTTP API調用時設置超時時間。若是被調用服務花費的時間太長,到時間後就會取消請求:

func main() {
    // 建立一個超時時間爲100毫秒的上下文
    ctx := context.Background()
    ctx, _ = context.WithTimeout(ctx, 100*time.Millisecond)

    // 建立一個訪問Google主頁的請求
    req, _ := http.NewRequest(http.MethodGet, "http://google.com", nil)
    // 將超時上下文關聯到建立的請求上
    req = req.WithContext(ctx)

    // 建立一個HTTP客戶端並執行請求
    client := &http.Client{}
    res, err := client.Do(req)
    // 若是請求失敗了,記錄到STDOUT
    if err != nil {
        fmt.Println("Request failed:", err)
        return
    }
    // 請求成功後打印狀態碼
    fmt.Println("Response received, status code:", res.StatusCode)
}

根據Google主頁響應你請求的速度,你將收到:

Response received, status code: 200

或者:

Request failed: Get http://google.com: context deadline exceeded

對於咱們來講一般都會收到第二條消息:)

context使用上的一些陷阱

儘管Go中的上下文取消功能是一種多功能工具,可是在繼續操做以前,你須要牢記一些注意事項。其中最重要的是,上下文只能被取消一次。若是您想在同一操做中傳播多個錯誤,那麼使用上下文取消可能不是最佳選擇。使用取消上下文的場景是你實際上確實要取消某項操做,而不只僅是通知下游進程發生了錯誤。 還須要記住的另外一件事是,應該將相同的上下文實例傳遞給你可能要取消的全部函數和goroutine

WithTimeoutWithCancel包裝一個已經支持取消功能的上下文將會形成多種可能會致使你的上下文被取消的狀況,應該避免這種二次包裝。

相關文章
相關標籤/搜索