在Go語言併發編程中,用一個goroutine來處理一個任務,而它又會建立多個goroutine來負責不一樣子任務的場景很是常見。以下圖git
這些場景中,每每會須要在API邊界之間以及過程之間傳遞截止時間、取消信號或與其它請求相關的數據github
誰是性能卡點呢?得通知它們任務取消了。golang
這時候就能夠使用Context
了。context包在Go1.7的時候被加入到官方庫中。sql
context包的內容能夠歸納爲,一個接口,四個具體實現,還有六個函數。編程
Context接口提供了四個方法,下面是Context的接口安全
type Context interface { Deadline() (deadline time.Time, ok bool) Done() <-chan struct{} Err() error Value(key interface{}) interface{} }
emptyCtx本質上是一個整型, *emptyCtx對Context接口的實現,只是簡單的返回nil,false,實際上什麼也沒作。以下代碼所示:併發
type emptyCtx int 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 }
Background和TODO這兩個函數內部都會建立emptyCtx函數
var ( background = new(emptyCtx) todo = new(emptyCtx) ) func Background() Context { return background } func TODO() Context { return todo }
其中Background主要用於在初始化時獲取一個Context(從代碼中可知本質是一個*emptyCtx,而emptCtx本質上是一個Int),這就是Background()函數返回的變量結構。性能
而TODO()函數,官方文檔建議在原本應該使用外層傳遞的ctx而外層卻沒有傳遞的地方使用,就像函數名稱表達的含義同樣,留下一個TODO。spa
再來看cancelCtx類型,cancleCtx定義以下
// cancelCtx能夠被取消。 取消後,它也會取消全部實現取消方法的子級。 type cancelCtx struct { Context mu sync.Mutex // protects following fields done 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 } func (c *cancelCtx) Value(key interface{}) interface{} { if key == &cancelCtxKey { return c } return c.Context.Value(key) } func (c *cancelCtx) Done() <-chan struct{} { c.mu.Lock() if c.done == nil { c.done = make(chan struct{}) } d := c.done c.mu.Unlock() return d } func (c *cancelCtx) Err() error { c.mu.Lock() err := c.err c.mu.Unlock() return err }
這是一種可取消的Context,done用於獲取該Context的取消通知,children用於存儲以當前節點爲根節點的全部可取消的Context,以便在根節點取消時,能夠把它們一併取消,err用於存儲取消時指定的錯誤信息,而這個mu就是用來保護這幾個字段的鎖,以保障cancelCtx是線程安全的。
而WithCancel函數,能夠把一個Context包裝爲cancelCtx,並提供一個取消函數,調用它能夠Cancel對應的Context
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) { if parent == nil { panic("cannot create context from nil parent") } c := newCancelCtx(parent) propagateCancel(parent, &c) return &c, func() { c.cancel(true, Canceled) } }
示例代碼:
ctx := context.Background() ctx1, cancel := context.WithCancel(ctx)
再來看timerCtx,timerCtx定義以下
type timerCtx struct { cancelCtx timer *time.Timer // Under cancelCtx.mu. deadline time.Time }
它在cancelCtx的基礎上,又封裝了一個定時器和一個截止時間,這樣既能夠根據須要主動取消,也能夠在到達deadline時,經過timer來觸發取消動做。
要注意,這個timer也會由cancelCtx.mu來保護,確保取消操做也是線程安全的。
經過WithDeadline和WithTimeout函數,均可以建立timerCtx,區別是WithDeadline函數須要指定一個時間點,而WithTimeout函數接收一個時間段。
接下來,咱們基於ctx1構造一個timerCtx
ctx := context.Background() ctx1, cancel := context.WithCancel(ctx) deadline := time.Now().Add(time.Second) ctx2, cancel := context.WithDeadline(ctx1, deadline)
這個定時器會在deadline到達時,調用cancelCtx的取消函數,如今能夠看到ctx2是基於ctx1建立的,而ctx1又是基於ctx建立的,基於每一個Context能夠建立多個Context,這樣就造成了一個Context樹,每一個節點均可以有零個或多個子節點,可取消的Context都會被註冊到離它最近的、可取消的祖先節點中。對ctx2來講離它最新的、可取消的祖先節點是ctx1
因此在ctx1這裏的children map中,會增長ctx2這組鍵值對
若是ctx2先取消,就只會影響到以它爲根節點的Context,而若是ctx1先取消,就能夠根據children map中的記錄,把ctx1子節點中全部可取消的Context所有Cancel掉。
最後來看valueCtx類型
首先來看valueCtx的定義
type valueCtx struct { Context key, val interface{} }
它用來支持鍵值對打包,WithValue函數能夠給Context附加一個鍵值對信息,這樣就能夠經過Context傳遞數據了
var keyA string = "keyA" ctx := context.Background() ctxA := context.WithValue(ctx, keyA, "valA")
如今咱們給ctx附加一個鍵值對keyA=>valA,變量ctxA也是Context接口類型,動態類型爲*valueCtx,data指向一個valueCtx結構體,第一個字段是它的父級Context,key和val字段都是空接口類型,keyA的動態類型爲string,動態值是string類型的變量keyA,val的動態類型一樣是string,動態值爲valA,
下面咱們再基於ctxA,附加一個key相等但val不相等的鍵值對keyA=>eggo,ctxC的動態值指向這樣一個valueCtx,父級Context天然是ctxA,key與ctxA中的相同,可是val的值與ctxA中的不相等
經過ctxC獲取kyA和keyC對應的值時會發現keyC覆蓋了keyA對應的val,要找到緣由,就要先看看Value方法是怎麼工做的
func (c *valueCtx) Value(key interface{}) interface{} { if c.key == key { return c.val } return c.Context.Value(key) }
首先它會比較當前Context中的key是否等於要查找的key,由於keyA等於keyC,因此對keyA的查找會直接鎖定到ctxC這裏的val,於是出現了子節點覆蓋父節點數據的狀況,爲了規避這種狀況,最好不要直接使用string、int這些基礎類型做爲Key,而是用自定義類型包裝一下,就像下面這樣,把keyA定義爲keytypea類型,keyC定義爲keytypec類型,這樣再次經過ctxC獲取keyA時,由於key的類型不相同,第一步key相等性比較不經過,就會委託父節點繼續查找,進而找到正確的val
因此說valueCtx之間經過Context字段造成了一個鏈表結構,使用Context傳遞數據時還要注意,Context自己本着不可改變(immutable)的模式設計的,因此不要試圖修改ctx裏保存的值,在http、sql相關的庫中,都提供了對Context的支持,方便咱們在處理請求時,實現超時自動取消,或傳遞請求相關的控制數據等等
瞭解了context包中,一個接口,四種具體實現,以及六個函數的基本狀況,有助於咱們理解Context的工做原理
整理自: