Go:Context 和傳播取消

context 包 [1] 在 Go 1.7 中引入,它爲我們提供了一種在應用程序中處理 context 的方法。這些 context 可以爲取消任務或定義超時提供幫助。通過 context 傳播請求的值也很有用,但對於本文,我們將重點關注 context 的取消功能。

默認的 contexts

Go 的 context 包基於 TODO 或者 Background 來構建 context。

var (
   background = new(emptyCtx)
   todo       = new(emptyCtx)
)

func Background() Context {
   return background
}

func TODO() Context {
   return todo
}

我們可以看到,它們都是空的 context。這是簡單的 context,永遠不會被取消,也不會帶任何值。

你可以將 background context 作爲主 context,並將其派生出新的 context。基於這些,你不應直接在包中使用 context; 它應該在你的主函數中使用。如果要使用 net/http 包構建服務,則主 context 將由請求提供:

net/http/request.go
func (r *Request) Context() context.Context {
   if r.ctx != nil {
      return r.ctx
   }
   return context.Background()
}

如果你在自己的包中工作並且沒有任何可用的 context,在這種情況下你應該使用 TODO context。通常,或者如果你對必須使用的 context 有任何疑問,可以使用 TODO context。現在我們知道了主 context,讓我們看看它是如何派生子 context 的。

Contexts 樹

父 context 派生出的子 context 會在在其內部結構中創建一個和父 context 之間的聯繫:

type cancelCtx struct {
   Context

   mu       sync.Mutex
   done     chan struct{}
   children map[canceler]struct{}
   err      error
}

children 字段跟蹤以此 context 創建的所有子項,而 Context 指向創建當前項的 context。

以下是創建一些 context 和子 context 的示例:

每個 context 都相互鏈接,如果我們取消 “C” context,所有它的孩子也將被取消。Go 會對它的子 context 進行循環逐個取消:

context/context.go
func (c *cancelCtx) cancel(removeFromParent bool, err error) {
   [...]
   for child := range c.children {
      child.cancel(false, err)
   }
   [...]
}

取消結束,將不會通知父 context。如果我們取消 C1,它只會通知 C11 和 C12:

這種取消傳播允許我們定義更高級的例子,這些例子可以幫助我們根據主 context 處理多個 / 繁重的工作。

取消傳播

讓我們通過 Goroutine A 和 B 來展示一個取消的例子,它們將並行運行,因爲擁有共同的 context ,當一個發生錯誤取消時,另外一個也會被取消:

如果沒有任何錯誤發生,每個過程都將正常運行。我在每個任務上添加了一條跟蹤,這樣我們就可以看到一棵樹:

A - 100ms
B - 200ms
    -> A1 - 100ms
        -> A11 - 50ms
    -> B1 - 100ms
        -> A12 - 300ms
    -> B2 - 100ms
        -> B21 - 150ms

每項任務都執行得很好。現在,讓我們嘗試讓 A11 模擬出錯誤:

A - 100ms
    -> A1 - 100ms
B - 200ms
        -> A11 - error
        -> A12 - cancelled
    -> B1 - 100ms
    -> B2 - cancelled
    -> B21 - cancelled

我們可以看到,當 B2 和 B21 被取消的同時,A12 被中斷,以避免做出不必要的處理(譯者注:B2 B21 的取消不是因爲 A12 中斷,應該是想表達併發安全的意思):

我們可以在這裏看到 context 對於多個 Goroutine 是線程安全的。實際上,有可能是因爲我們之前在結構中看到的 mutex,它保證了對 context 的併發安全。

context 泄漏

正如我們在內部結構中看到的那樣,當前 context 在 Context 屬性中保持其父級的鏈接,而父級將當前 context 保留在 children 屬性中。對 cancel 函數的調用將把當前 context 中的子項清除並刪除與父項的鏈接:

func (c *cancelCtx) cancel(removeFromParent bool, err error) {
   [...]
   c.children = nil

   if removeFromParent {
      removeChild(c.Context, c)
   }
}

如果未調用 cancel 函數,則主 context 將始終保持與它創建的 context 的鏈接,從而導致可能的內存泄漏。

可以用 go vet 命令來檢查是否泄漏,它將對可能的泄漏拋出警告:

the cancel function returned by context.WithCancel should be called, not discarded, to avoid a context leak

總結

context 包還有另外兩個利用 cancel 函數的函數:WithTimeoutWithDeadline。在定義的超時 / 截止時間後,它們都會自動觸發 cancel 函數。

context 包還提供了一個 WithValue 的方法,它允許我們在 context 中存儲任何對鍵 / 值。此功能受到爭議,因爲它不提供明確的類型控制,可能導致糟糕的編程習慣。如果你想了解 WithValue 的更多信息,我建議你閱讀 Jack Lindamood 關於 context 值的文章 [2]。


via: https://medium.com/@blanchon.vincent/go-context-and-cancellation-by-propagation-7a808bbc889c

作者:Vincent Blanchon[3] 譯者:咔嘰咔嘰 [4] 校對:zhoudingding[5]

本文由 GCTT[6] 原創編譯,Go 中文網 [7] 榮譽推出,發佈在 Go 語言中文網公衆號,轉載請聯繫我們授權。

參考資料

[1]

context 包: https://blog.golang.org/context

[2]

Jack Lindamood 關於 context 值的文章: https://medium.com/@cep21/how-to-correctly-use-context-context-in-go-1-7-8f2c0fafdf39

[3]

Vincent Blanchon: https://medium.com/@blanchon.vincent

[4]

咔嘰咔嘰: https://github.com/watermelo

[5]

zhoudingding: https://github.com/dingdingzhou

[6]

GCTT: https://github.com/studygolang/GCTT

[7]

Go 中文網: https://studygolang.com/

福利

我爲大家整理了一份從入門到進階的 Go 學習資料禮包,包含學習建議:入門看什麼,進階看什麼。關注公衆號 「polarisxu」,回覆 ebook 獲取;還可以回覆「進羣」,和數萬 Gopher 交流學習。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/fIovF3EXRUsrx4EL-oz90w