圖解Go語言的context瞭解程式語言核心實現原始碼
基礎築基
基於執行緒的程式語言中的一些設計
ThreadGroup
ThreadGroup是基於執行緒併發的程式語言中常用的一個概念,當一個執行緒派生出一個子執行緒後通常會加入父執行緒的執行緒組(未指定執行緒組的情況下)中, 最後可以通過ThreadGroup來控制一組執行緒的退出等操作, 然後在go語言中goroutine沒有明確的這種parent/children的關係,如果想退出當前呼叫鏈上的所有goroutine則需要用到context
ThreadLocal
在基於執行緒的程式語言語言中,通常可以基於ThreadLocal來進行一些執行緒本地的儲存,本質上是通過一個Map來進行key/value的儲存,而在go裡面並沒有ThreadLocal的設計,在key/value傳遞的時候,除了通過引數來進行傳遞,也可以通過context來進行上下文資訊的傳遞
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那是不是就可以了呢
帶有超時context
如果要實現一個超時控制,通過上面的context的parent/child機制,其實我們只需要啟動一個定時器,然後在超時的時候,直接將當前的context給cancel掉,就可以實現監聽在當前和下層的額context.Done()的goroutine的退出
Background與TODO
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
結構體
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
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
關注公告號閱讀更多原始碼分析文章
更多文章關注 www.sreguide.com
本文由部落格一文多發平臺 OpenWrite 釋出