詳解golang中Context超時控制與原理
Context
在Go語言圈子中流行著一句話:
Never start a goroutine without knowing how it will stop。
翻譯:如果你不知道協(xié)程如何退出,就不要使用它。
在創(chuàng)建協(xié)程時,我們可能還會再創(chuàng)建一些別的子協(xié)程,那么這些協(xié)程的退出就成了問題。在Go1.7之后,Go官方引入了Context來實現(xiàn)協(xié)程的退出。不僅如此,Context還提供了跨協(xié)程、甚至是跨服務的退出管理。
Context本身的含義是上下文,我們可以理解為它內部攜帶了超時信息、退出信號,以及其他一些上下文相關的值(例如攜帶本次請求中上下游的唯一標識trace_id)。由于Context攜帶了上下文信息,父子協(xié)程之間就可以”聯(lián)動“
了。
Context標準庫
在Context標準庫中重要的結構 context.Context其實是一個接口,它提供了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的過期時間。Deadline第一個返回值表示Context的過期時間,第二個返回值表示是否設置了過期時間,如果多次調用Deadline方法會返回相同的值。
Done是使用最頻繁的方法,它會返回一個通道。一般的做法是調用者在select中監(jiān)聽該通道的信號,如果該通道關閉則表示服務超時或異常,需要執(zhí)行后續(xù)退出邏輯。多次調用Done方法會返回相同的通道。
通道關閉后,Err方法回返回退出的原因。
Value方法返回指定Key對應的value,這是Context攜帶的值。Key必須是可比較的,一般用法Key是一個全局變量,通過context.WithValue將key存儲到Context中,并通過Context.Value方法退出。
Context是一個接口,這意味著需要有對應的具體實現(xiàn)。用戶可以自己實現(xiàn)Context接口,并嚴格遵守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標準庫中的實現(xiàn)。
前三個函數都用于派生出有退出功能的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函數回返回一個子Context和cancel方法。子Context會在兩種情況下觸發(fā)退出:一種情況是調用者主動調用了返回的cancel方法;另一種情況是當參數中的父Context退出時,子Context將級聯(lián)退出。
- WithTimeout函數指定超時時間。當超時發(fā)生后,子Context將退出。因此,子Context的退出有三種時機,一種是父Context退出;一種是超時退出;最后一種是主動調用cancel函數退出。
- WithDeadline和WithTimeout函數的處理方法相似,不過它們的參數指定的是最后到期的時間。
- WithValue函數會返回帶key-value的子Context。
Context實踐
eg:
下面的代碼中childCtx是preCtx的子Context,其設置的超時時間為300ms。但是preCtx的超時時間為100ms,因此父Context退出后,子Context會立即退出,實際的等待時間只有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退出的時間差接近100ms:
pre during: 104 child during: 104
當我們把preCtx的超時時間修改為500ms時:
preCtx ,_:= context.WithTimeout(ctx,500*time.Millisecond)
從新的輸出中可以看出,子協(xié)程的退出不會影響父協(xié)程的退出。
child during: 304 pre during: 500
Context底層原理
Context在很大程度上利用了通道的一個特性:通道在close時,會通知所有監(jiān)聽它的協(xié)程。
每個派生出的子Context都會創(chuàng)建一個新的退出通道,這樣,只要組織好Context之間的關系,就可以實現(xiàn)繼承鏈上退出信號的傳遞。如圖所示的三個協(xié)程中,關閉通道A會連帶關閉調用鏈上的通道B,通道B會關閉通道C。
要使用context的退出功能,需要調用WithCancel或WithTimeout,派生出一個新的結構Context。WithCancel底層對應的結構為cancelCtx,WithTimeout底層對應的結構為timerCtx,timerCtx包裝了cancelCtx,并存儲了超時時間。
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第一個字段保留了父Context的信息。children字段則保存了當前Context派生的子Context的信息,每個Context都會有一個單獨的done通道。
而WithDeadline函數會先判斷父Context設置的超時時間是否比當前Context的超時時間短,如果是,那么子協(xié)程會隨著父Context的退出而退出,沒有必要再設置定時器。
當我們使用了標準庫中默認的Context實現(xiàn)時,propagateCancel函數將子Context加入父協(xié)程的children哈希表中,并開啟一個定時器。當定時器到期時,會調用cancel方法關閉通道,級聯(lián)關閉當前Context派生的子Context,并取消與父Context的綁定關系。這種特性就產生了調用鏈上連鎖的退出反應。
以上就是詳解golang中Context超時控制與原理的詳細內容,更多關于golang Context超時的資料請關注腳本之家其它相關文章!