go context剖析之源碼分析

開篇

源碼面前,了無祕密。本文做爲context分析系列的第二篇,會從源碼的角度來分析context如何實現所承諾的功能及內在特性。本篇主要從如下四個角度闡述: context中的接口、context有哪些類型、context的傳遞實現、context的層級取消觸發實現。安全

context中的接口

上一篇go context剖析之使用技巧中能夠看到context包自己包含了數個導出函數,包括WithValue、WithTimeout等,不管是最初構造context仍是傳導context,最核心的接口類型都是context.Context,任何一種context也都實現了該接口,包括value context。bash

type Context interface {
    Deadline() (deadline time.Time, ok bool)
    Done() <-chan struct{}
    Err() error
    Value(key interface{}) interface{}
}
複製代碼

到底有幾種context?

既然context都須要實現Context,那麼包括不直接可見(非導出)的結構體,一共有幾種context呢?答案是4種併發

  • 類型一: emptyCtx,context之源頭

emptyCtx定義以下函數

type emptyCtx int
複製代碼

爲了減輕gc壓力,emptyCtx實際上是一個int,而且以do nothing的方式實現了Context接口,還記得context包裏面有兩個初始化context的函數post

func Background() Context
func TODO() Context
複製代碼

這兩個函數返回的實現類型即爲emptyCtx,而在contex包中實現了兩個emptyCtx類型的全局變量: background、todo,其定義以下ui

var (
	background = new(emptyCtx)
	todo       = new(emptyCtx)
)
複製代碼

上述兩個函數依次對應這兩個全局變量。到這裏咱們能夠很肯定地說context的根節點就是一個int全局變量,而且Background()和TODO()是同樣的。因此千萬不要用nil做爲context,而且從易於理解的角度出發,未考慮清楚是否傳遞、如何傳遞context時用TODO,其餘狀況都用Background(),如請求入口初始化contextspa

  • 類型二: cancelCtx,cancel機制之靈魂

cancelCtx的cancel機制是手工取消、超時取消的內部實現,其定義以下設計

type cancelCtx struct {
	Context

	mu       sync.Mutex
	done     chan struct{}
	children map[canceler]struct{}
	err      error 
}
複製代碼

這裏的mu是context併發安全的關鍵、done是通知的關鍵、children存儲結構是內部最經常使用傳導context的方式。3d

  • 類型三: timerCtx,cancel機制的場景補充

timerCtx內部包含了cancelCtx,而後經過定時器,實現了到時取消的功能,定義以下code

type timerCtx struct {
	cancelCtx
	timer *time.Timer // Under cancelCtx.mu.

	deadline time.Time
}
複製代碼

這裏deadline只作記錄、String()等邊緣功能,timer纔是關鍵。

  • 類型四: valueCtx,傳值

valueCtx是四個類型的最後一個,只用來傳值,固然也能夠傳遞,全部context均可以傳遞,定義以下

type valueCtx struct {
	Context
	key, val interface{}
}
複製代碼

因爲有的人認爲context應該只用來傳值、有的人認爲context的cancel機制纔是核心,因此對於valueCtx也在下面作了一個單獨的介紹,你們能夠經過把握內部實現後按照本身的業務場景作一個取捨(傳值能夠用一個全局結構體、map之類)。

value context的底層是map嗎?

在上面valueCtx的定義中,咱們能夠看出其實value context底層不是一個map,而是每個單獨的kv映射都對應一個valueCtx,當傳遞多個值時就要構造多個ctx。同時,這要是value contex不能自低向上傳遞值的緣由。

valueCtx的key、val都是接口類型,在調用WithValue的時候,內部會首先經過反射肯定key是否可比較類型(同map中的key),而後賦值key

在調用Value的時候,內部會首先在本context查找對應的key,若是沒有找到會在parent context中遞歸尋找,這也是value能夠自頂向下傳值的緣由。

context是如何傳遞的

首先能夠明確,任何一種context都具備傳遞性,而傳遞性的內在機制能夠理解爲: 在調用WithCancel、WithTimeout、WithValue時如何處理父子context。從傳遞性的角度來講,幾種With*函數內部都是經過propagateCancel這個函數來實現的,下面以WithCancel函數爲例

func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
	c := newCancelCtx(parent)
	propagateCancel(parent, &c)
	return &c, func() { c.cancel(true, Canceled) }
}
複製代碼

newCancelCtx是cancelCtx賦值父context的過程,而propagateCancel創建父子context之間的聯繫。

propagateCance定義以下

func propagateCancel(parent Context, child canceler) {
	if parent.Done() == nil {
		return // parent is never canceled
	}
	if p, ok := parentCancelCtx(parent); ok {// context包內部能夠直接識別、處理的類型
		p.mu.Lock()
		if p.err != nil {
			// parent has already been canceled
			child.cancel(false, p.err)
		} else {
			if p.children == nil {
				p.children = make(map[canceler]struct{})
			}
			p.children[child] = struct{}{}
		}
		p.mu.Unlock()
	} else {// context包內部不能直接處理的類型,好比type A struct{context.Context},這種靜默包含的方式
		go func() {
			select {
			case <-parent.Done():
				child.cancel(false, parent.Err())
			case <-child.Done():
			}
		}()
	}
}
複製代碼

1.若是parent.Done是nil,則不作任何處理,由於parent context永遠不會取消,好比TODO()、Background()、WithValue等。 2.parentCancelCtx根據parent context的類型,返回bool型ok,ok爲真時須要創建parent對應的children,並保存parent->child映射關係(cancelCtx、timerCtx這兩種類型會創建,valueCtx類型會一直向上尋找,而循環往上找是由於cancel是必須的,而後找一種最合理的。),這裏children的key是canceler接口,並不能處理全部的外部類型,因此會有else,示例見上述代碼註釋處。對於其餘外部類型,不創建直接的傳遞關係。 parentCancelCtx定義以下

func parentCancelCtx(parent Context) (*cancelCtx, bool) {
	for {
		switch c := parent.(type) {
		case *cancelCtx:
			return c, true
		case *timerCtx:
			return &c.cancelCtx, true
		case *valueCtx:
			parent = c.Context // 循環往上尋找
		default:
			return nil, false
		}
	}
}
複製代碼

context是如何觸發取消的

上文在闡述傳遞性時的實現時,也包含了一部分取消機制的代碼,這裏不會再列出源碼,可是會依據上述源碼進行說明。對於幾種context,傳遞過程大同小異,可是取消機制有所不一樣,針對每種類型,我會一一解釋。不一樣類型的context能夠在一條鏈路進行取消,可是每個context的取消只會被一種條件觸發,因此下面會單獨介紹下每一種context的取消機制(組合取消的場景,按照先到先得的原則,不管那種條件觸發的,都會傳遞調用cancel)。這裏有兩個設計很關鍵:

  1. cancel函數是冪等的,能夠被屢次調用。
  2. context中包含done channel能夠用來確認是否取消、通知取消。
  • cancelCtx類型

cancelCtx會主動進行取消,在自頂向下取消的過程當中,會遍歷children context,而後依次主動取消。 cancel函數定義以下

func (c *cancelCtx) cancel(removeFromParent bool, err error) {
	if err == nil {
		panic("context: internal error: missing cancel error")
	}
	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(c.done)
	}
	for child := range c.children {
		// NOTE: acquiring the child's lock while holding parent's lock.
		child.cancel(false, err)
	}
	c.children = nil
	c.mu.Unlock()

	if removeFromParent {
		removeChild(c.Context, c)
	}
}
複製代碼
  • timerCtx類型

WithTimeout是經過WithDeadline來實現的,均對應timerCtx類型。經過parentCancelCtx函數的定義咱們知道,timerCtx也會記錄父子context關係。可是timerCtx是經過timer定時器觸發cancel調用的,部分實現以下

if c.err == nil {
	    c.timer = time.AfterFunc(dur, func() {
	        c.cancel(true, DeadlineExceeded)
            })
	}
複製代碼
  • 靜默包含context

這裏暫時只想到了靜默包含即type A struct{context.Context}的狀況。經過parentCancelCtx和propagateCancel咱們知道這種context不會創建父子context的直接聯繫,可是會經過單獨的goroutine去檢測done channel,來肯定是否須要觸發鏈路上的cancel函數,實現見propagateCancel的else部分。

結尾

context的實現並不複雜,可是在實際開發中確能帶來不小的便利性。篇一力求你們可以按場景對號入座熟練地使用context,篇二但願你們可以從源碼層面瞭解到context的實現,在一些極端場景下,如靜默包含context,也能從容權衡利弊,作到知其然知其因此然,謝謝。

相關文章
相關標籤/搜索