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 函數的函數:WithTimeout
和 WithDeadline
。在定義的超時 / 截止時間後,它們都會自動觸發 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