圖解Go語言的context了解編程語言核心實現源碼


基礎築基

基於線程的編程語言中的一些設計

ThreadGroup

image.png
ThreadGroup是基於線程並發的編程語言中常用的一個概念,當一個線程派生出一個子線程后通常會加入父線程的線程組(未指定線程組的情況下)中, 最后可以通過ThreadGroup來控制一組線程的退出等操作, 然后在go語言中goroutine沒有明確的這種parent/children的關系,如果想退出當前調用鏈上的所有goroutine則需要用到context

ThreadLocal

在基於線程的編程語言語言中,通常可以基於ThreadLocal來進行一些線程本地的存儲,本質上是通過一個Map來進行key/value的存儲,而在go里面並沒有ThreadLocal的設計,在key/value傳遞的時候,除了通過參數來進行傳遞,也可以通過context來進行上下文信息的傳遞

context典型應用場景

場景 實現 原理
上下文信息傳遞 WithValue 通過一個內部的key/value屬性來進行鍵值對的保存,不可修改,只能通過覆蓋的方式來進行值得替換
退出通知 WithCancel 通過監聽通知的channel來進行共同退出的通知

上下文數據的遞歸獲取

image.png
因為在go的context里面並沒有使用map進行數據保存,所以實際獲取的時候,是從當前層開始逐層的進行向上遞歸,直至找到某個匹配的key

其實我們類比ThreadGroup,因為goroutine本身並沒有上下級的概念,但其實我們可以通過context來實現傳遞數據的父子關系,可以在一個goroutine中設定context數據,然后傳遞給派生出來的goroutine

取消的通知

image.png
既然通過context來構建parent/child的父子關系,在實現的過程中context會向parent來注冊自身,當我們取消某個parent的goroutine, 實際上上會遞歸層層cancel掉自己的child context的done chan從而讓整個調用鏈中所有監聽cancel的goroutine退出

那如果一個child context的done chan為被初始化呢?那怎么通知關閉呢,那直接給你一個closedchan已經關閉的channel那是不是就可以了呢

帶有超時context

image.png
如果要實現一個超時控制,通過上面的context的parent/child機制,其實我們只需要啟動一個定時器,然后在超時的時候,直接將當前的context給cancel掉,就可以實現監聽在當前和下層的額context.Done()的goroutine的退出

Background與TODO

image.png
Backgroud其實從字面意思就很容易理解,其實構建一個context對象作為root對象,其本質上是一個共享的全局變量,通常在一些系統處理中,我們都可以使用該對象作為root對象,並進行新context的構建來進行上下文數據的傳遞和統一的退出控制

那TODO呢?通常我們會給自己立很多的todo list,其實這里也一樣,我們雖然構建了很多的todo list, 但大多數人其實啥也不會做,在很多的函數調用的過程中都會傳遞但是通常又不會使用,比如你既不會監聽退出,也不會從里面獲取數據,TODO跟Background一樣,其背后也是返回一個全局變量

不可變性

通常我們使用context都是做位一個上下文的數據傳遞,比如一次http request請求的處理,但是如果當這次請求處理完成,其context就失去了意義,后續不應該繼續重復使用一個context, 之前如果超時或者已經取消,則其狀態不會發生改變

源碼實現

context接口

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

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

image.png

結構體

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

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
}

cancel

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

image.png
timerCtx主要是用於實現WithDeadline和WithTimer兩個context實現,其繼承了cancelCtx接口,同時還包含一個timer.Timer定時器和一個deadline終止實現

2.4.1 結構體

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()
}

valueCtx

其內部通過一個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

設計目標

propagateCancel主要設計目標就是當parent context取消的時候,進行child context的取消, 這就會有兩種模式:
1.parent取消的時候通知child進行cancel取消
2.parent取消的時候調用child的層層遞歸取消

parentCancelCtx

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,就簡單了許多, 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) }
}

Backgroup與TODO

在很多底層的中間件的調用中都會通過context進行信息的傳遞,其中最常用的就是Backgroup和Todo, 雖然都是基於emptyCtx實現,但Backgroup則更傾向於作為一個parent context進行后續整個調用鏈context的root使用,而TODO通常則表明后續不會進行任何操作,僅僅是因為參數需要傳遞使用

原文鏈接 http://www.sreguide.com/go/context.html
微信號:baxiaoshi2020
關注公告號閱讀更多源碼分析文章 21天大棚
更多文章關注 www.sreguide.com
本文由博客一文多發平台 OpenWrite 發布


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM