Go:Context 和傳播取消
默認(rèn)的 contexts
Go 的 context 包基于 TODO 或者 Background 來構(gòu)建 context。
var?(
???background?=?new(emptyCtx)
???todo???????=?new(emptyCtx)
)
func?Background()?Context?{
???return?background
}
func?TODO()?Context?{
???return?todo
}
我們可以看到,它們都是空的 context。這是簡單的 context,永遠不會被取消,也不會帶任何值。
你可以將 background context 作為主 context,并將其派生出新的 context?;谶@些,你不應(yīng)直接在包中使用 context;它應(yīng)該在你的主函數(shù)中使用。如果要使用 net/http 包構(gòu)建服務(wù),則主 context 將由請求提供:
net/http/request.go
func?(r?*Request)?Context()?context.Context?{
???if?r.ctx?!=?nil?{
??????return?r.ctx
???}
???return?context.Background()
}
如果你在自己的包中工作并且沒有任何可用的 context,在這種情況下你應(yīng)該使用 TODO context。通常,或者如果你對必須使用的 context 有任何疑問,可以使用 TODO context?,F(xiàn)在我們知道了主 context,讓我們看看它是如何派生子 context 的。
Contexts 樹
父 context 派生出的子 context 會在在其內(nèi)部結(jié)構(gòu)中創(chuàng)建一個和父 context 之間的聯(lián)系:
type?cancelCtx?struct?{
???Context
???mu???????sync.Mutex
???done?????chan?struct{}
???children?map[canceler]struct{}
???err??????error
}
children 字段跟蹤以此 context 創(chuàng)建的所有子項,而 Context 指向創(chuàng)建當(dāng)前項的 context。
以下是創(chuàng)建一些 context 和子 context 的示例:

每個 context 都相互鏈接,如果我們?nèi)∠?“C” context,所有它的孩子也將被取消。Go 會對它的子 context 進行循環(huán)逐個取消:
context/context.go
func?(c?*cancelCtx)?cancel(removeFromParent?bool,?err?error)?{
???[...]
???for?child?:=?range?c.children?{
??????child.cancel(false,?err)
???}
???[...]
}
取消結(jié)束,將不會通知父 context。如果我們?nèi)∠?C1,它只會通知 C11 和 C12:

這種取消傳播允許我們定義更高級的例子,這些例子可以幫助我們根據(jù)主 context 處理多個/繁重的工作。
取消傳播
讓我們通過 Goroutine A 和 B 來展示一個取消的例子,它們將并行運行,因為擁有共同的 context ,當(dāng)一個發(fā)生錯誤取消時,另外一個也會被取消:

如果沒有任何錯誤發(fā)生,每個過程都將正常運行。我在每個任務(wù)上添加了一條跟蹤,這樣我們就可以看到一棵樹:
A - 100ms
B - 200ms
-> A1 - 100ms
-> A11 - 50ms
-> B1 - 100ms
-> A12 - 300ms
-> B2 - 100ms
-> B21 - 150ms
每項任務(wù)都執(zhí)行得很好。現(xiàn)在,讓我們嘗試讓 A11 模擬出錯誤:
A - 100ms
-> A1 - 100ms
B - 200ms
-> A11 - error
-> A12 - cancelled
-> B1 - 100ms
-> B2 - cancelled
-> B21 - cancelled
我們可以看到,當(dāng) B2 和 B21 被取消的同時,A12 被中斷,以避免做出不必要的處理(譯者注:B2 B21 的取消不是因為 A12 中斷,應(yīng)該是想表達并發(fā)安全的意思):

我們可以在這里看到 context 對于多個 Goroutine 是線程安全的。實際上,有可能是因為我們之前在結(jié)構(gòu)中看到的 mutex,它保證了對 context 的并發(fā)安全。
context 泄漏
正如我們在內(nèi)部結(jié)構(gòu)中看到的那樣,當(dāng)前 context 在 Context 屬性中保持其父級的鏈接,而父級將當(dāng)前 context 保留在 children 屬性中。對 cancel 函數(shù)的調(diào)用將把當(dāng)前 context 中的子項清除并刪除與父項的鏈接:
func?(c?*cancelCtx)?cancel(removeFromParent?bool,?err?error)?{
???[...]
???c.children?=?nil
???if?removeFromParent?{
??????removeChild(c.Context,?c)
???}
}
如果未調(diào)用 cancel 函數(shù),則主 context 將始終保持與它創(chuàng)建的 context 的鏈接,從而導(dǎo)致可能的內(nèi)存泄漏。
可以用 go vet 命令來檢查是否泄漏,它將對可能的泄漏拋出警告:
the cancel function returned by context.WithCancel should be called, not discarded, to avoid a context leak
總結(jié)
context 包還有另外兩個利用 cancel 函數(shù)的函數(shù):WithTimeout 和 WithDeadline。在定義的超時/截止時間后,它們都會自動觸發(fā) cancel 函數(shù)。
context 包還提供了一個 WithValue 的方法,它允許我們在 context 中存儲任何對鍵/值。此功能受到爭議,因為它不提供明確的類型控制,可能導(dǎo)致糟糕的編程習(xí)慣。如果你想了解 WithValue 的更多信息,我建議你閱讀Jack Lindamood 關(guān)于 context 值的文章[2]。
via: https://medium.com/@blanchon.vincent/go-context-and-cancellation-by-propagation-7a808bbc889c
作者:Vincent Blanchon[3]譯者:咔嘰咔嘰[4]校對:zhoudingding[5]
本文由 GCTT[6] 原創(chuàng)編譯,Go 中文網(wǎng)[7] 榮譽推出,發(fā)布在 Go語言中文網(wǎng)公眾號,轉(zhuǎn)載請聯(lián)系我們授權(quán)。
參考資料
context 包: https://blog.golang.org/context
[2]Jack Lindamood 關(guān)于 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 中文網(wǎng): https://studygolang.com/
?最后,再打個廣告吧。
雙十一快到了,阿里云也開始搞活動了,剛好我這邊可以帶大家?白Piao?阿里云的服務(wù)器。
說白了就是大家?可以一分錢不花,就可以領(lǐng)到服務(wù)器,規(guī)格是 2c2g(2vcpu 2G memory) 的機器。
有需要的可以加我下微信,備注『服務(wù)器』,我統(tǒng)一拉群,帶大家一起薅羊毛。
