ThreadGroup是基於線程併發的編程語言中經常使用的一個概念,當一個線程派生出一個子線程後一般會加入父線程的線程組(未指定線程組的狀況下)中, 最後能夠經過ThreadGroup來控制一組線程的退出等操做, 而後在go語言中goroutine沒有明確的這種parent/children的關係,若是想退出當前調用鏈上的全部goroutine則須要用到contexthtml
在基於線程的編程語言語言中,一般能夠基於ThreadLocal來進行一些線程本地的存儲,本質上是經過一個Map來進行key/value的存儲,而在go裏面並無ThreadLocal的設計,在key/value傳遞的時候,除了經過參數來進行傳遞,也能夠經過context來進行上下文信息的傳遞編程
場景 | 實現 | 原理 |
---|---|---|
上下文信息傳遞 | WithValue | 經過一個內部的key/value屬性來進行鍵值對的保存,不可修改,只能經過覆蓋的方式來進行值得替換 |
退出通知 | WithCancel | 經過監聽通知的channel來進行共同退出的通知 |
由於在go的context裏面並無使用map進行數據保存,因此實際獲取的時候,是從當前層開始逐層的進行向上遞歸,直至找到某個匹配的key微信
其實咱們類比ThreadGroup,由於goroutine自己並無上下級的概念,但其實咱們能夠經過context來實現傳遞數據的父子關係,能夠在一個goroutine中設定context數據,而後傳遞給派生出來的goroutine併發
既然經過context來構建parent/child的父子關係,在實現的過程當中context會向parent來註冊自身,當咱們取消某個parent的goroutine, 實際上上會遞歸層層cancel掉本身的child context的done chan從而讓整個調用鏈中全部監聽cancel的goroutine退出編程語言
那若是一個child context的done chan爲被初始化呢?那怎麼通知關閉呢,那直接給你一個closedchan已經關閉的channel那是否是就能夠了呢ide
若是要實現一個超時控制,經過上面的context的parent/child機制,其實咱們只須要啓動一個定時器,而後在超時的時候,直接將當前的context給cancel掉,就能夠實現監聽在當前和下層的額context.Done()的goroutine的退出函數
Backgroud其實從字面意思就很容易理解,其實構建一個context對象做爲root對象,其本質上是一個共享的全局變量,一般在一些系統處理中,咱們均可以使用該對象做爲root對象,並進行新context的構建來進行上下文數據的傳遞和統一的退出控制源碼分析
那TODO呢?一般咱們會給本身立不少的todo list,其實這裏也同樣,咱們雖然構建了不少的todo list, 但大多數人其實啥也不會作,在不少的函數調用的過程當中都會傳遞可是一般又不會使用,好比你既不會監聽退出,也不會從裏面獲取數據,TODO跟Background同樣,其背後也是返回一個全局變量學習
一般咱們使用context都是作位一個上下文的數據傳遞,好比一次http request請求的處理,可是若是當此次請求處理完成,其context就失去了意義,後續不該該繼續重複使用一個context, 以前若是超時或者已經取消,則其狀態不會發生改變ui
type Context interface { // Deadline返回一個到期的timer定時器,以及當前是否以及到期 Deadline() (deadline time.Time, ok bool) // Done在當前上下文完成後返回一個關閉的通道,表明當前context應該被取消,以便goroutine進行清理工做 // WithCancel:負責在cancel被調用的時候關閉Done // WithDeadline: 負責在最後其期限過時時關閉Done // WithTimeout:負責超時後關閉done Done() <-chan struct{} // 若是Done通道沒有被關閉則返回nil // 不然則會返回一個具體的錯誤 // Canceled 被取消 // DeadlineExceeded 過時 Err() error // 返回對應key的value Value(key interface{}) interface{} }
emptyCtx是一個不會被取消、沒有到期時間、沒有值、不會返回錯誤的context實現,其主要做爲context.Background()和context.TODO()返回這種root context或者不作任何操做的context
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 } func (e *emptyCtx) String() string { switch e { case background: return "context.Background" case todo: return "context.TODO" } return "unknown empty Context" }
比較有意思的實現時emptyCtx的String方法,該方法能夠返回當前context的具體類型,好比是Background仍是TODO, 由於background和todo是兩個全局變量,這裏經過取其地址來進行對應類型的判斷
cancelCtx結構體內嵌了一個Context對象,即其parent context,同時內部還經過children來保存全部能夠被取消的context的接口,後續噹噹前context被取消的時候,只須要調用全部canceler接口的context就能夠實現當前調用鏈的取消
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 }
Done操做返回當前的一個chan 用於通知goroutine退出
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) cancel(removeFromParent bool, err error) { if err == nil { panic("context: internal error: missing cancel error") } // context一旦被某個操做操做觸發取消後,就不會在進行任何狀態的修改 c.mu.Lock() if c.err != nil { c.mu.Unlock() return // already canceled } c.err = err if c.done == nil { c.done = closedchan } else { // close當前chan close(c.done) } // 調用全部children取消 for child := range c.children { child.cancel(false, err) } c.children = nil c.mu.Unlock() // 是否須要從parent context中移除,若是是當前context的取消操做,則須要進行該操做 // 不然,則上層context會主動進行child的移除工做 if removeFromParent { removeChild(c.Context, c) } }
timerCtx主要是用於實現WithDeadline和WithTimer兩個context實現,其繼承了cancelCtx接口,同時還包含一個timer.Timer定時器和一個deadline終止實現
timerCtx
type timerCtx struct { cancelCtx timer *time.Timer // timer定時器 deadline time.Time //終止時間 }
取消方法就很簡單了首先進行cancelCtx的取消流程,而後進行自身的定時器的Stop操做,這樣就能夠實現取消了
func (c *timerCtx) cancel(removeFromParent bool, err error) { c.cancelCtx.cancel(false, err) if removeFromParent { // Remove this timerCtx from its parent cancelCtx's children. removeChild(c.cancelCtx.Context, c) } c.mu.Lock() if c.timer != nil { c.timer.Stop() // 中止定時器 c.timer = nil } c.mu.Unlock() }
其內部經過一個key/value進行值得保存,若是當前context不包含着值就會層層向上遞歸
type valueCtx struct { Context key, val interface{} } func (c *valueCtx) String() string { return fmt.Sprintf("%v.WithValue(%#v, %#v)", c.Context, c.key, c.val) } func (c *valueCtx) Value(key interface{}) interface{} { if c.key == key { return c.val } return c.Context.Value(key) }
propagateCancel主要設計目標就是當parent context取消的時候,進行child context的取消, 這就會有兩種模式: 1.parent取消的時候通知child進行cancel取消 2.parent取消的時候調用child的層層遞歸取消
context能夠任意嵌套組成一個N層樹形結構的context, 結合上面的兩種模式,當能找到parent爲cancelCtx、timerCtx任意一種的時候,就採用第二種模式,由parent來調用child的cancel完成整個調用鏈的退出,反之則採用第一種模式監聽Done
func parentCancelCtx(parent Context) (*cancelCtx, bool) { for { switch c := parent.(type) { case *cancelCtx: return c, true // 找到最近支持cancel的parent,由parent進行取消操做的調用 case *timerCtx: return &c.cancelCtx, true // 找到最近支持cancel的parent,由parent進行取消操做的調用 case *valueCtx: parent = c.Context // 遞歸 default: return nil, false } } }
func propagateCancel(parent Context, child canceler) { if parent.Done() == nil { return // parent is never canceled } if p, ok := parentCancelCtx(parent); ok { p.mu.Lock() if p.err != nil { // parent has already been canceled // 若是發現parent已經取消就直接進行取消 child.cancel(false, p.err) } else { if p.children == nil { p.children = make(map[canceler]struct{}) } // 不然加入parent的children map中 p.children[child] = struct{}{} } p.mu.Unlock() } else { go func() { select { case <-parent.Done(): // 監聽parent DOne完成, 此處也不會向parent進行註冊 child.cancel(false, parent.Err()) case <-child.Done(): } }() } }
有了上面的基礎學習WithDeadline,就簡單了許多, WithDeadline會給定一個截止時間, 能夠經過當前時間計算須要等待多長時間取消便可
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) { if cur, ok := parent.Deadline(); ok && cur.Before(d) { // The current deadline is already sooner than the new one. return WithCancel(parent) } c := &timerCtx{ cancelCtx: newCancelCtx(parent), deadline: d, } // 監聽parent的取消,或者向parent註冊自身 propagateCancel(parent, c) dur := time.Until(d) if dur <= 0 { // 已通過期 c.cancel(true, DeadlineExceeded) // deadline has already passed return c, func() { c.cancel(false, Canceled) } } c.mu.Lock() defer c.mu.Unlock() if c.err == nil { c.timer = time.AfterFunc(dur, func() { // 構建一個timer定時器,到期後自動調用cancel取消 c.cancel(true, DeadlineExceeded) }) } // 返回取消函數 return c, func() { c.cancel(true, Canceled) } }
在不少底層的中間件的調用中都會經過context進行信息的傳遞,其中最經常使用的就是Backgroup和Todo, 雖然都是基於emptyCtx實現,但Backgroup則更傾向於做爲一個parent context進行後續整個調用鏈context的root使用,而TODO一般則代表後續不會進行任何操做,僅僅是由於參數須要傳遞使用
原文連接 http://www.sreguide.com/go/context.html 微信號:baxiaoshi2020 關注公告號閱讀更多源碼分析文章 更多文章關注 www.sreguide.com 本文由博客一文多發平臺 OpenWrite 發佈