Golang context 實現原理
本篇文章是基于小徐先生的文章的修改和個人注解,要查看原文可以點擊上述的鏈接查看
目前我這篇文章的go語言版本是1.24.1
context上下文
context被當作第一個參數(官方建議),并且不斷的傳遞下去,基本上一個項目代碼到處都是context,但是你們真的知道他有什么作用嗎?
接下來就來看看這個golang世界中的典型工具吧
func main() {ctx,cancel := context.WithTimeout(context.Background(),10 * time.Second)defer cancel()go Monitor(ctx)time.Sleep(20 * time.Second)
}func Monitor(ctx context.Context) {for {fmt.Print("monitor")}
}
但是他到底時如何處理并發控制和實現呢?
接下來就來深入看看他的原理和使用
一.context包介紹
context
可以用來在goroutine
之間傳遞上下文信息,相同的context
可以傳遞給運行在不同goroutine
中的函數,上下文對于多個goroutine
同時使用是安全的,context
包定義了上下文類型,可以使用background
、TODO
創建一個上下文,在函數調用鏈之間傳播context
,也可以使用WithDeadline
、WithTimeout
、WithCancel
或 WithValue
創建的修改副本替換它,聽起來有點繞,其實總結起就是一句話:context
的作用就是在不同的goroutine
之間同步請求特定的數據、取消信號以及處理請求的截止日期。
目前我們常用的一些庫都是支持context
的,例如gin
、database/sql
等庫都是支持context
的,這樣更方便我們做并發控制了,只要在服務器入口創建一個context
上下文,不斷透傳下去即可。
二.context的使用
2.1 context.Context
他是核心數據結構,看一下它的樣子吧:
type Context interface {Deadline() (deadline time.Time, ok bool)Done() <-chan struct{}Err() errorValue(key any) any
}
Context 為 interface,定義了四個核心 api:
? Deadline:返回 context 的過期時間
? Done:返回 context 中的 channel
? Err:返回錯誤
? Value:返回 context 中的對應 key 的值
2.2 標準error
var Canceled = errors.New("context canceled")var DeadlineExceeded error = deadlineExceededError{}type deadlineExceededError struct{}func (deadlineExceededError) Error() string { return "context deadline exceeded" }
func (deadlineExceededError) Timeout() bool { return true }
func (deadlineExceededError) Temporary() bool { return true }
? Canceled:context 被 cancel 時會報此錯誤
? DeadlineExceeded:context 超時時會報此錯誤
三.類的實現
3.1 emptyCtx
通過看它的源碼我們會發現,這個空的上下文其實就是實現了這個context這個接口,對于實現的4個方法都是返回的nil,這就是為什么說他是empty
在之前的版本中他可能是int類型,后來已經被修改了
type emptyCtx struct{}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 any) any {return nil
}
? emptyCtx 是一個空的 context
? Deadline 方法會返回一個公元元年時間以及 false 的 flag,標識當前 context 不存在過期時間;
? Done 方法返回一個 nil 值,用戶無論往 nil 中寫入或者讀取數據,均會陷入阻塞;
? Err 方法返回的錯誤永遠為 nil;
? Value 方法返回的 value 同樣永遠為 nil.
3.1.1 context.Background() & context.TODO()
context包主要提供了兩種方式創建context:
context.Backgroud()
context.TODO()
我們在看代碼的時候,經常可以看到不是Background就是todo作為上下文的起始,那他們有什么區別呢?
兩者的區別
這兩個函數其實只是互為別名,沒有差別,官方給的定義是:
context.Background
是上下文的默認值,所有其他的上下文都應該從它衍生(Derived)出來。context.TODO
應該只在不確定應該使用哪種上下文時使用;
所以在大多數情況下,我們都使用context.Background
作為起始的上下文向下傳遞。
看一下兩者的底層是什么?
兩者其實都是一個對context的一個繼承
type backgroundCtx struct{ emptyCtx }
type todoCtx struct{ emptyCtx }func Background() Context {return backgroundCtx{}
}func TODO() Context {return todoCtx{}
}
看到這里,你會發現上面的兩種方式是創建根context
,不具備任何功能,具體實踐其實還是要依靠context
包提供的With
系列函數來進行派生:
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
func WithValue(parent Context, key, val interface{}) Context
這四個函數都要基于父Context
衍生,通過這些函數,就創建了一顆Context樹,樹的每個節點都可以有任意多個子節點,節點層級可以有任意多個,畫個圖表示一下:
基于一個父Context
可以隨意衍生,其實這就是一個Context
樹,樹的每個節點都可以有任意多個子節點,節點層級可以有任意多個,每個子節點都依賴于其父節點,例如上圖,我們可以基于Context.Background
衍生出四個子context
:ctx1.0-cancel
、ctx2.0-deadline
、ctx3.0-timeout
、ctx4.0-withvalue
,這四個子context
還可以作為父context
繼續向下衍生,即使其中ctx1.0-cancel
節點取消了,也不影響其他三個父節點分支。
創建context
方法和context
的衍生方法就這些,關于這些with函數,會在后續的使用中看到
3.1.2 With類函數
WithCancel(parent Context)
用途:創建一個新的上下文和取消函數。當調用取消函數時,所有派生自這個上下文的操作將被通知取消。
應用場景:當一個長時間運行的操作需要能夠被取消時。例如,用戶在網頁中點擊“取消”按鈕時,相關的數據庫或 HTTP 請求應立即停止。
2. WithDeadline(parent Context, d time.Time)
用途:創建一個新的上下文,該上下文在指定的時間點自動取消。
應用場景:在請求處理時設置最大執行時間。例如,調用外部 API 時,如果響應時間超過預期,將自動取消請求,以避免無效的等待。
3. WithTimeout(parent Context, timeout time.Duration)
用途:創建一個新的上下文,它會在指定的持續時間內自動取消。
應用場景:適用于設置操作的超時時間,確保系統不會在某個操作上無休止地等待。常用于網絡請求或長時間運行的任務。
4. WithValue(parent Context, key, val interface{})
用途:創建一個新的上下文,并將鍵值對存儲在該上下文中。
應用場景:在處理請求時,將特定的數據(如用戶身份信息、RequestID)在處理鏈中傳遞,而不需要在每個函數參數中顯式傳遞。
3.2 cancelCtx
接下來看一下第二個實現的類吧,看名字就能看出來他是一個帶有取消功能的上下文。
type cancelCtx struct {Contextmu sync.Mutex done atomic.Value children map[canceler]struct{} // 這里是一個set{}err error cause error
}type canceler interface {cancel(removeFromParent bool, err, cause error)Done() <-chan struct{}
}// 這里體現了goland的一個編程哲學
// 作為一個父context,它只需要關注子類的這兩個方法即可,
// 它的子類可能更有能力,但是與父親無關,只需要知道他是否還存在即可
// 會通過就近生成interface的方式,把無關的信息都屏蔽掉。
// 也就是誰使用誰聲明誰管理
? 繼承了一個 context 作為其父 context. 可見,cancelCtx 必然為某個 context 的子 context;
? 內置了一把鎖,用以協調并發場景下的資源獲取;
? done:實際類型為 chan struct{},即用以反映 cancelCtx 生命周期的通道;
? children:一個 set,指向 cancelCtx 的所有子 context;
? err:記錄了當前 cancelCtx 的錯誤. 必然為某個 context 的子 context;
? cause:是在go1.20之后加入的字段,主要作用適用于記錄導致context被取消的具體原因
在這里,要加入一些其他的內容----同步調用和異步調用
同步調用,其實就是一種串行的方式,也就是我們平時寫的程序,他是一步一步進行下去,類似一條鏈的形式。
而異步調用則是開辟協程,并且不會阻塞主線程,并且主線程對子協程的感知能力很弱,開辟了多個子協程,就會形成類似樹的形式
就會導致,主線程對子協程的管理能力下降,從而致使協程無法回收,最后導致協程泄露的一個問題。
在創建協程方面,我們要知道一點,如果不知道你創建的協程什么時候結束,你就不應該去創建,不應該濫用并發。
如何解決這個協程的控制呢?
那么今天的主角就是cancelCtx了
先說cancelCtx繼承了Context,對其方法進行了一個重寫,但是并沒有對Deadline方法重寫,而是直接繼承的父類的。Deadline不進行重寫是因為他沒有過期取消的能力。
func (c *cancelCtx) Value(key any) any {// 這里為什么要加入這個判斷,在后續會介紹,主要就是用于判斷// 其自身是否是一個cancelCtx類型,這個cancelCtxKey是一個定值if key == &cancelCtxKey {return c}return value(c.Context, key)
}func (c *cancelCtx) Done() <-chan struct{} {d := c.done.Load()if d != nil {return d.(chan struct{})}c.mu.Lock()defer c.mu.Unlock()d = c.done.Load()if d == nil {d = make(chan struct{})c.done.Store(d)}return d.(chan struct{})
}func (c *cancelCtx) Err() error {c.mu.Lock()err := c.errc.mu.Unlock()return err
}
3.2.1 WithCancel取消控制
既然想實現這種父子聯動的行為,就輪到了With下的一個函數WithCancel函數,通過這個函數從而得到一個cancelCtx對象,從而實現一個對子協程的一個控制
func main() {ctx,cancel := context.WithCancel(context.Background())// 以context.Background()為父,創建得到一個子context和cancelgo Speak(ctx)time.Sleep(10*time.Second)cancel()time.Sleep(1*time.Second)
}func Speak(ctx context.Context) {for range time.Tick(time.Second){select {case <- ctx.Done():fmt.Println("我要閉嘴了")returndefault:fmt.Println("balabalabalabala")}}
}
來對這個例子做出一個解釋:
select就相當于是一個多路復用,進行一個監聽的操作,通過這個WithCancel獲取上下文和取消函數
當調用這個cancel函數的時候,就會直接通過這個ctx.Done發送這個取消機制,從而實現一個控制的效果
這里的操作也就是我們常說的超時控制了,當然cancel并沒有涉及到超時,他是通過調用cancel()才可以實現一個關閉。
這個結構體就是cancel取消函數的結構體,他返回的是一個函數類型,所以調用的時候需要加上()
來看下這個函數的具體流程這里先提前告知一點,如果停止一個cancelCtx,則這個它下面所有的子上下文都將被殺死,具體的操作看propagateCancel函數
type CancelFunc func()func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {c := withCancel(parent)//Canceled是一個error,自定義的錯誤信息return c, func() { c.cancel(true, Canceled, nil) }
}func withCancel(parent Context) *cancelCtx {if parent == nil {panic("cannot create context from nil parent")}c := &cancelCtx{}c.propagateCancel(parent, c)return c
}func (c *cancelCtx) propagateCancel(parent Context, child canceler) {c.Context = parent// 這一步就是說父親是emptyCtx,他就沒有必要取消// 所以沒有必要大費周折的取消它,直接就返回就行done := parent.Done()if done == nil {return // parent is never canceled}// 如果父親被取消了,那兒子也應該直接取消,記錄一下取消的錯誤和原因select {case <-done:// parent is already canceledchild.cancel(false, parent.Err(), Cause(parent))returndefault:}// 如果我的父也是cancelCtx,則我需要將孩子加入map里面if p, ok := parentCancelCtx(parent); ok {// parent is a *cancelCtx, or derives from one.p.mu.Lock()if p.err != nil {// parent has already been canceledchild.cancel(false, p.err, p.cause)} else {if p.children == nil {p.children = make(map[canceler]struct{})}p.children[child] = struct{}{}}p.mu.Unlock()return}//檢查父Context是否支持AfterFunc,也就是一個回調機制// if a, ok := parent.(afterFuncer); ok {// parent implements an AfterFunc method.c.mu.Lock()stop := a.AfterFunc(func() {child.cancel(false, parent.Err(), Cause(parent))})c.Context = stopCtx{Context: parent,stop: stop,}c.mu.Unlock()return}// 開啟一個守護協程,時刻監聽,第一個判斷父親是不是被終止了// 如果父親被終止了,就應該給孩子也砍一刀,讓他們也都終止// 如果孩子被終止了,那就被終止了,什么也不需要處理,傳播具有單向性goroutines.Add(1)go func() {select {case <-parent.Done():child.cancel(false, parent.Err(), Cause(parent))case <-child.Done():}}()
}
parentCancelCtx用于判斷是不是cancelCtx類型
func (c *cancelCtx) Value(key any) any {// 這里為什么要加入這個判斷,在后續會介紹,主要就是用于判斷// 其自身是否是一個cancelCtx類型,這個cancelCtxKey是一個定值if key == &cancelCtxKey {return c}return value(c.Context, key)
}func parentCancelCtx(parent Context) (*cancelCtx, bool) {done := parent.Done()if done == closedchan || done == nil {return nil, false}p, ok := parent.Value(&cancelCtxKey).(*cancelCtx)if !ok {return nil, false}pdone, _ := p.done.Load().(chan struct{})if pdone != done {return nil, false}return p, true
}
再看一下返回的閉包函數吧
cancelCtx.cancel 方法有三個入參,第一個 removeFromParent 是一個 bool 值,表示當前 context 是否需要從父 context 的 children set 中刪除;第二個 err 則是 cancel 后需要展示的錯誤,第三個則表示導致錯誤原因。
func (c *cancelCtx) cancel(removeFromParent bool, err, cause error) {if err == nil {panic("context: internal error: missing cancel error")}if cause == nil {cause = err}c.mu.Lock()if c.err != nil {c.mu.Unlock()return // already canceled}c.err = errc.cause = caused, _ := c.done.Load().(chan struct{})if d == nil {// closedchan 這里是一個全局chan// 關閉,從而取消監聽,Store就是為了確保原子性和可見性c.done.Store(closedchan)} else {close(d)}for child := range c.children {// NOTE: acquiring the child's lock while holding parent's lock.child.cancel(false, err, cause)}c.children = nilc.mu.Unlock()if removeFromParent {removeChild(c.Context, c)}
}// removeChild removes a context from its parent.
func removeChild(parent Context, child canceler) {if s, ok := parent.(stopCtx); ok {s.stop()return}p, ok := parentCancelCtx(parent)if !ok {return}p.mu.Lock()if p.children != nil {delete(p.children, child)}p.mu.Unlock()
}
3.3 timerCtx
接下來看第三個實現的類
type timerCtx struct {cancelCtxtimer *time.Timer // Under cancelCtx.mu.deadline time.Time
}
timerCtx 在 cancelCtx 基礎上又做了一層封裝,除了繼承 cancelCtx 的能力之外,新增了一個 time.Timer 用于定時終止 context;另外新增了一個 deadline 字段用于字段 timerCtx 的過期時間.
這樣就有了實現時停的操作,它對Dealine進行了一個重寫,其他都是繼承的cancelCtx的
Deadline返回的是 deadline time.Time
3.3.1WithTimeout和WithDeadline
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {return WithDeadline(parent, time.Now().Add(timeout))
}func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {return WithDeadlineCause(parent, d, nil)
}
這兩個函數他們的參數就可以看出區別,context.WithTimeout是指經過的時間段,而WithDeadline則是指時間點。
這里屬于是超時取消。
3.4 valueCtx
type valueCtx struct {Contextkey, val any
}
說一下這個valueCtx吧,在不同位置設置的value其實在查找方面是有問題的,比如你在最下面那一層去存放,也就是B下面的子節點存放value,只有A和B才可以訪問這個value,C和D是無法訪問到的。
3.4.1WithValue
func WithValue(parent Context, key, val any) Context {if parent == nil {panic("cannot create context from nil parent")}if key == nil {panic("nil key")}if !reflectlite.TypeOf(key).Comparable() {panic("key is not comparable")}return &valueCtx{parent, key, val}
}
通過這個函數來設置value值。