詳解golang中Context超時(shí)控制與原理
Context
在Go語言圈子中流行著一句話:
Never start a goroutine without knowing how it will stop。
翻譯:如果你不知道協(xié)程如何退出,就不要使用它。
在創(chuàng)建協(xié)程時(shí),我們可能還會(huì)再創(chuàng)建一些別的子協(xié)程,那么這些協(xié)程的退出就成了問題。在Go1.7之后,Go官方引入了Context來實(shí)現(xiàn)協(xié)程的退出。不僅如此,Context還提供了跨協(xié)程、甚至是跨服務(wù)的退出管理。
Context本身的含義是上下文,我們可以理解為它內(nèi)部攜帶了超時(shí)信息、退出信號(hào),以及其他一些上下文相關(guān)的值(例如攜帶本次請求中上下游的唯一標(biāo)識(shí)trace_id)。由于Context攜帶了上下文信息,父子協(xié)程之間就可以”聯(lián)動(dòng)“
了。
Context標(biāo)準(zhǔn)庫
在Context標(biāo)準(zhǔn)庫中重要的結(jié)構(gòu) context.Context其實(shí)是一個(gè)接口,它提供了Deadline、Done、Err、Value這4種方法:
type Context interface { Deadline() (deadline time.Time, ok bool) Done() <-chan struct{} Err() error Value(key interface{}) interface{} }
Deadline方法用于返回Context的過期時(shí)間。Deadline第一個(gè)返回值表示Context的過期時(shí)間,第二個(gè)返回值表示是否設(shè)置了過期時(shí)間,如果多次調(diào)用Deadline方法會(huì)返回相同的值。
Done是使用最頻繁的方法,它會(huì)返回一個(gè)通道。一般的做法是調(diào)用者在select中監(jiān)聽該通道的信號(hào),如果該通道關(guān)閉則表示服務(wù)超時(shí)或異常,需要執(zhí)行后續(xù)退出邏輯。多次調(diào)用Done方法會(huì)返回相同的通道。
通道關(guān)閉后,Err方法回返回退出的原因。
Value方法返回指定Key對應(yīng)的value,這是Context攜帶的值。Key必須是可比較的,一般用法Key是一個(gè)全局變量,通過context.WithValue將key存儲(chǔ)到Context中,并通過Context.Value方法退出。
Context是一個(gè)接口,這意味著需要有對應(yīng)的具體實(shí)現(xiàn)。用戶可以自己實(shí)現(xiàn)Context接口,并嚴(yán)格遵守Context接口。
func (*emptyCtx) Deadline() (deadline time.Time, ok bool) { return } func (*emptyCtx) Done() <-chan struct{} { return nil } func (*emptyCtx) Err() error { return nil } func (*emptyCtx) Value(key interface{}) interface{} { return nil }
因此,要具體使用Context,需要派生出新的Context。我們使用的最多的還是Go標(biāo)準(zhǔn)庫中的實(shí)現(xiàn)。
前三個(gè)函數(shù)都用于派生出有退出功能的Context。
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) func WithValue(parent Context, key, val interface{}) Context
- WithCancel函數(shù)回返回一個(gè)子Context和cancel方法。子Context會(huì)在兩種情況下觸發(fā)退出:一種情況是調(diào)用者主動(dòng)調(diào)用了返回的cancel方法;另一種情況是當(dāng)參數(shù)中的父Context退出時(shí),子Context將級聯(lián)退出。
- WithTimeout函數(shù)指定超時(shí)時(shí)間。當(dāng)超時(shí)發(fā)生后,子Context將退出。因此,子Context的退出有三種時(shí)機(jī),一種是父Context退出;一種是超時(shí)退出;最后一種是主動(dòng)調(diào)用cancel函數(shù)退出。
- WithDeadline和WithTimeout函數(shù)的處理方法相似,不過它們的參數(shù)指定的是最后到期的時(shí)間。
- WithValue函數(shù)會(huì)返回帶key-value的子Context。
Context實(shí)踐
eg:
下面的代碼中childCtx是preCtx的子Context,其設(shè)置的超時(shí)時(shí)間為300ms。但是preCtx的超時(shí)時(shí)間為100ms,因此父Context退出后,子Context會(huì)立即退出,實(shí)際的等待時(shí)間只有100ms。
func main() { ctx := context.Background() before := time.Now() preCtx, _ := context.WithTimeout(ctx, 100*time.Millisecond) go func() { childCtx, _ := context.WithTimeout(preCtx, 300*time.Millisecond) select { case <-childCtx.Done(): after := time.Now() fmt.Println("child during:", after.Sub(before).Milliseconds()) } }() select { case <-preCtx.Done(): after := time.Now() fmt.Println("pre during:", after.Sub(before).Milliseconds()) } }
這是輸出如下,父Context與子Context退出的時(shí)間差接近100ms:
pre during: 104 child during: 104
當(dāng)我們把preCtx的超時(shí)時(shí)間修改為500ms時(shí):
preCtx ,_:= context.WithTimeout(ctx,500*time.Millisecond)
從新的輸出中可以看出,子協(xié)程的退出不會(huì)影響父協(xié)程的退出。
child during: 304 pre during: 500
Context底層原理
Context在很大程度上利用了通道的一個(gè)特性:通道在close時(shí),會(huì)通知所有監(jiān)聽它的協(xié)程。
每個(gè)派生出的子Context都會(huì)創(chuàng)建一個(gè)新的退出通道,這樣,只要組織好Context之間的關(guān)系,就可以實(shí)現(xiàn)繼承鏈上退出信號(hào)的傳遞。如圖所示的三個(gè)協(xié)程中,關(guān)閉通道A會(huì)連帶關(guān)閉調(diào)用鏈上的通道B,通道B會(huì)關(guān)閉通道C。
要使用context的退出功能,需要調(diào)用WithCancel或WithTimeout,派生出一個(gè)新的結(jié)構(gòu)Context。WithCancel底層對應(yīng)的結(jié)構(gòu)為cancelCtx,WithTimeout底層對應(yīng)的結(jié)構(gòu)為timerCtx,timerCtx包裝了cancelCtx,并存儲(chǔ)了超時(shí)時(shí)間。
type cancelCtx struct { Context mu sync.Mutex // protects following fields done atomic.Value // of chan struct{}, created lazily, closed by first cancel call children map[canceler]struct{} // set to nil by the first cancel call err error // set to non-nil by the first cancel call cause error // set to non-nil by the first cancel call } type timerCtx struct { cancelCtx timer *time.Timer // Under cancelCtx.mu. deadline time.Time }
cancelCtx第一個(gè)字段保留了父Context的信息。children字段則保存了當(dāng)前Context派生的子Context的信息,每個(gè)Context都會(huì)有一個(gè)單獨(dú)的done通道。
而WithDeadline函數(shù)會(huì)先判斷父Context設(shè)置的超時(shí)時(shí)間是否比當(dāng)前Context的超時(shí)時(shí)間短,如果是,那么子協(xié)程會(huì)隨著父Context的退出而退出,沒有必要再設(shè)置定時(shí)器。
當(dāng)我們使用了標(biāo)準(zhǔn)庫中默認(rèn)的Context實(shí)現(xiàn)時(shí),propagateCancel函數(shù)將子Context加入父協(xié)程的children哈希表中,并開啟一個(gè)定時(shí)器。當(dāng)定時(shí)器到期時(shí),會(huì)調(diào)用cancel方法關(guān)閉通道,級聯(lián)關(guān)閉當(dāng)前Context派生的子Context,并取消與父Context的綁定關(guān)系。這種特性就產(chǎn)生了調(diào)用鏈上連鎖的退出反應(yīng)。
以上就是詳解golang中Context超時(shí)控制與原理的詳細(xì)內(nèi)容,更多關(guān)于golang Context超時(shí)的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
一文帶你了解Go中跟蹤函數(shù)調(diào)用鏈的實(shí)現(xiàn)
這篇文章主要為大家詳細(xì)介紹了go如何實(shí)現(xiàn)一個(gè)自動(dòng)注入跟蹤代碼,并輸出有層次感的函數(shù)調(diào)用鏈跟蹤命令行工具,感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2023-11-11