最具研讀價值的 Go 源碼之一:context 包

前言

你瞭解 Context 中的回溯鏈和樹結構嗎?想知道 Context 如何觸發級聯取消嗎?本文將換個角度聊一聊 golang 中的 context,讓你真正理解什麼是 Context。

context 包中的代碼雖然只有 600 多行,但已經成爲了併發控制、超時控制的標準做法,可以說是真正的短小而精悍,是十分值得研讀的 Go 源碼之一。本文首先從整體的視角解析了 context 的主要接口和函數,分析了其中重要結構的實現關係,以及存儲所用的數據結構;隨後針對 context 接口不同的實現源碼進行了詳細的解析,可以幫助大家更有效理解不同 Context 的實現原理。(關於具體使用,我們下期再聊)

注:本文依據 go 版本 1.20.7 源碼進行講解 源碼地址:src/context/context.go

1.context 簡介

1.1 context 是什麼?

context 在 Go 1.7 版本被引入標準庫,包 context 定義了 Context 類型,它攜帶跨 API 邊界和進程之間的截止日期、取消信號和其他請求範圍的值。context 在 Golang 中的作用是爲了提供一種在函數之間**「傳遞請求作用域數據、控制請求執行時間、處理併發任務」**等功能的機制,以便更加有效地管理和控制請求的執行。特別是在處理併發請求和微服務架構中,context 的作用更加凸顯。

1.2 爲什麼需要 context

舉一個常見的例子:大量 http 請求不斷地訪問服務器,每一個請求都會開多個協程去處理這個請求的業務邏輯,例如:獲取用戶信息(基本信息、權限信息、其他額外的信息等),如果下游業務處理邏輯發生異常,長時間未返回處理結果,上游長時間的等待將會導致嚴重的超時,業務服務可能會因爲協程沒有釋放導致協程泄漏,極端情況還會引發服務器雪崩。因此,協程之間能夠進行事件通知並且能控制協程的生命週期非常重要,這時候就應該使用 context 包了,context 主要就是用來在多個協程中設置截止日期、同步信號,傳遞請求相關值。每一次 context 都會從頂層一層一層的傳遞到下面一層的協程中,當上面的 context 取消的時候,下面所有的 context 也會隨之取消。

因此,在 Golang 中引入 context 的主要原因是爲了 「更好地管理併發請求」「控制請求的執行」。在處理併發請求的情況下,可能會涉及到多個併發任務、超時控制、任務取消等需求,而 context 提供了一種標準化的方式來處理這些問題。具體來說,引入 context 主要有以下幾個原因:

  1. 「控制請求執行時間」:在處理 HTTP 請求或者其他併發任務時,可以使用 「context」 來設置截止時間或者超時時間,確保請求不會無限期地執行。這對於避免資源的過度佔用和及時釋放資源非常重要。

  2. 「取消操作」「context」 提供了一種統一的機制來取消請求的執行。在某些情況下,可能需要取消已經啓動的任務,或者在某些條件下終止任務的執行,這時可以使用 「context」 來實現。

  3. 「傳遞請求作用域的數據」「context」 可以用於在多個函數之間傳遞請求的元數據,例如請求 ID、用戶身份信息、語言環境等。這在處理微服務架構或者處理 HTTP 請求時非常有用。

  4. 「跨 API 邊界傳遞請求作用域數據」:當請求需要經過多個 API 邊界時,可以使用 「context」 來傳遞請求作用域的數據,確保在整個請求處理鏈路中能夠獲取到必要的信息和控制。

  5. 「併發任務管理」:通過 context 可以控制多個併發任務的執行,比如通過一個 context 對象去取消多個併發任務的執行,或者等待多個任務中的一個完成。

總的來說,「context」 的使用場景涵蓋了處理併發請求、控制請求執行時間、取消操作、請求作用域數據傳遞等多個方面,適用於處理併發請求、微服務架構中的請求處理等多種場景。

2.context 源碼整體概覽

研讀源碼,最好先從整體視角分析一下包中函數、接口、類(Go 中多爲結構體)之間的結構關係。下圖畫出了 context 包中的主要函數、接口和類的關係,context 包主要由兩個接口(Context 、canceler) 以及四個結構體 (emptyCtx、valueCtx、cancelCtx、timerCtx) 組成,其中類的實現關係如圖所示。emptyCtx 一般作爲 root 節點使用;valueCtx、cancelCtx、timerCtx 通過不同的函數依據祖先 Context 派生出來,結構體中通過嵌入的方式擁有對祖先 Context 的回溯機制(被稱爲回溯鏈),後續會進行詳細介紹。

下邊這張表列舉了 context 包中所有重要的變量、函數、接口以及結構體,起到總覽全局和查找複習的作用,等閱讀完全文,可以再來回顧一下。

CHeINH

2.1 接口

2.1.1 Context 接口

type Context interface {
 // 返回一個 channel,用於判斷 context 是否結束
    // 多次調用同一個 context done 方法會返回相同的 channel
    // 當 context 被取消或者到了 deadline,返回一個被關閉的 channel
 Done() <-chan struct{}

    // 當 context 結束時纔會返回錯誤,有兩種情況
    // context 被主動調用 cancel 方法取消:Canceled
    // context 超時取消: DeadlineExceeded
 Err() error

 // 返回 context 是否會被取消以及自動取消時間(即 deadline)
 Deadline() (deadline time.Time, ok bool)

 // 獲取 key 對應的 value
 Value(key interface{}) interface{}
}

簡單介紹一下方法的作用:

2.1.2 canceler 接口

type canceler interface {
 cancel(removeFromParent bool, err, cause error)
 Done() <-chan struct{}
}

canceler 接口定義了兩個方法,實現了這兩個方法的 Context 爲可取消的 Context,比如:  *cancelCtx 和 *timerCtx。

2.2 實現 Context 接口的結構體

在閱讀源碼後會發現,Context 各種創建方法其實主要只使用到了 4 種類型的 Context 實現,也就是前文經常提到的四種實現,本小節就來簡單介紹一下這 4 種類型的 Context。

2.2.1 emptyCtx

emptyCtx 本質是一個 int 類型,正如其名 emptyCtx 實現的 Context 全部返回了 nil。它通常用於創建 root Context,標準庫中 context.Background()context.TODO() 返回的就是這個 emptyCtx。

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 any) any {
 return nil
}

2.2.2 cancelCtx

cancelCtx 是 context 包中十分重要的數據結構,*cancelCtx 是一個可被取消的 Context,*cancelCtx 實現了 canceler 接口,同時內嵌了 Context 作爲匿名字段,這樣就會被派生爲一個 Context。

type cancelCtx struct {
 Context

 mu       sync.Mutex            // protects following fields
 done     atomic.Value          // of 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
 cause    error                 // set to non-nil by the first cancel call
}

cancelCtx 結構中有一個字段 children map[canceler]struct{} 用於維護 parent canceler 與 children canceler 之間的關係,後面 WithCancel 篇章會詳細講到樹結構的建立過程。

接下來看一下 *cancelCtx 的方法,首先是 Value() 方法:用於尋找第一個祖先(最近的祖先) *「cancelCtx 實例」

func (c *cancelCtx) Value(key any) any {
 if key == &cancelCtxKey {
  return c
 }
 return value(c.Context, key)
}

func value(c Context, key any) any {
 for {
  switch ctx := c.(type) {
  case *valueCtx:
   if key == ctx.key {
    return ctx.val
   }
   c = ctx.Context
  case *cancelCtx:
   if key == &cancelCtxKey {
    return c
   }
   c = ctx.Context
  case *timerCtx:
   if key == &cancelCtxKey {
    return ctx.cancelCtx
   }
   c = ctx.Context
  case *emptyCtx:
   return nil
  default:
   return c.Value(key)
  }
 }
}

cancelCtxKey 是 context 包中定義的私有變量,如果是 *cancelCtx 遇到 &cancelCtxKey 就會返回自己,否則沿着回溯鏈往上尋找,看看有沒有 parent 是 cancelCtx,如果有就返回尋找到的第一個祖先 cancelCtx。這個其實是複用了 Value 函數的回溯邏輯,從而在 Context 樹回溯鏈中遍歷時,可以找到給定 Context 的第一個祖先 *cancelCtx 實例。使用方式:p, ok := parent.Value(&cancelCtxKey).(*cancelCtx)

接下來是 「Done() 方法:返回一個只讀 channel 用於判斷是否取消。」

c.done 是 “懶漢式” 創建,只有調用了 Done() 方法的時候纔會被創建,而且只有 *cancelCtx 實現了非空 Done 函數。

func (c *cancelCtx) Done() <-chan struct{} {
    // 原子變量獲取存儲的通道信息
 d := c.done.Load()
 if d != nil {
        // 不爲 nil 則直接返回
  return d.(chan struct{})
 }
    // 併發鎖 - 要執行併發寫操作
 c.mu.Lock()
    // defer 解鎖
 defer c.mu.Unlock()
    // 二次判斷原子信息是否已經被其他協程寫入
 d = c.done.Load()
 if d == nil {
        // 沒有被寫入,則初始化 chan
  d = make(chan struct{})
        // 存入原子信息
  c.done.Store(d)
 }
    // 返回通道 chan
 return d.(chan struct{})
}

接下來 「Err() 方法」

func (c *cancelCtx) Err() error {
 c.mu.Lock()
 err := c.err
 c.mu.Unlock()
 return err
}

「String() 方法」

func contextName(c Context) string {
 if s, ok := c.(stringer); ok {
  return s.String()
 }
 return reflectlite.TypeOf(c).String()
}

func (c *cancelCtx) String() string {
 return contextName(c.Context) + ".WithCancel"
}

「cancel() 方法:該方法用於關閉 c.done 並且取消其 children,該方法不對外暴露,最終以函數返回值形式暴露出去:「cancel CancelFunc」。」

// cancel closes c.done, cancels each of c's children, and, if
// removeFromParent is true, removes c from its parent's children.
// cancel sets c.cause to cause if this is the first time c is canceled.
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()
    // err 不爲 nil,表示已經被取消過,那則不用繼續取消了
 if c.err != nil {
        // 解鎖
  c.mu.Unlock()
  return // already canceled
 }
    // 設置取消原因和錯誤
 c.err = err
 c.cause = cause
    // 取出 chan
 d, _ := c.done.Load().(chan struct{})
    // chan 未初始化
 if d == nil {
        // 直接存入已關閉的 chan,通知取消操作
  c.done.Store(closedchan)
 } else {
        // 關閉 chan,通知取消
  close(d)
 }
    // 祖先取消,孩子也得跟着取消(級聯取消)
 for child := range c.children {
  // NOTE: acquiring the child's lock while holding parent's lock.
        // 取消 child
  child.cancel(false, err, cause)
 }
    // 斷絕與所有後代的關係
 c.children = nil
    // 解鎖
 c.mu.Unlock()
 // 如果想斷絕與祖先的關係
 if removeFromParent {
        // 斷絕與祖先的關係
  removeChild(c.Context, c)
 }
}

// removeChild removes a context from its parent.
func removeChild(parent Context, child canceler) {
    // 回溯找到第一個祖先 *cancelCtx
    // 只有 *cancelCtx 中存在 children map[canceler]struct{},纔有父子關係
 p, ok := parentCancelCtx(parent)
    // 沒找到,意味着沒有父子關係,不用斷絕
 if !ok {
  return
 }
 p.mu.Lock()
 if p.children != nil {
        // 祖先的 children map 中移除對該孩子的關係
  delete(p.children, child)
 }
 p.mu.Unlock()
}

2.2.3 timerCtx

一個 timerCtx 攜帶一個定時器和一個截止時間,有了這兩個配置以後就可以在特定時間進行自動取消;它嵌入了一個 *cancelCtx 來實現 Done 和 Err,它通過停止計時器然後委託給 *cancelCtx.cancel 來實現取消。

// A timerCtx carries a timer and a deadline. It embeds a cancelCtx to
// implement Done and Err. It implements cancel by stopping its timer then
// delegating to cancelCtx.cancel.
type timerCtx struct {
 *cancelCtx
 timer *time.Timer // Under cancelCtx.mu.

 deadline time.Time
}

「Deadline() 方法:返回截止時間」

func (c *timerCtx) Deadline() (deadline time.Time, ok bool) {
 return c.deadline, true
}

String() 方法

func (c *timerCtx) String() string {
 return contextName(c.cancelCtx.Context) + ".WithDeadline(" +
  c.deadline.String() + " [" +
  time.Until(c.deadline).String() + "])"
}

「cancel() 方法」

func (c *timerCtx) cancel(removeFromParent bool, err, cause error) {
    // 委託給 cancelCtx 執行 cancel
 c.cancelCtx.cancel(false, err, cause)
 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()
}

2.2.4 valueCtx

valueCtx 內部同樣包含了一個 Context 接口實例,由原 Context 實例派生,valueCtx 結構中包含一對 key-value 用於存儲鍵值對,在調用 valueCtx.Value(key interface{}) 會進行遞歸向上查找 key 對應的 val,但是這個查找只負責查找 “直系” Context,也就是說可以無限遞歸查找 parent Context 是否包含這個 key,但是無法查找兄弟 Context 是否包含。

// A valueCtx carries a key-value pair. It implements Value for that key and
// delegates all other calls to the embedded Context.
type valueCtx struct {
 Context
 key, val any
}

其所包含方法較爲簡單,如下:

func (c *valueCtx) String() string {
 return contextName(c.Context) + ".WithValue(type " +
  reflectlite.TypeOf(c.key).String() +
  ", val " + stringify(c.val) + ")"
}

func (c *valueCtx) Value(key any) any {
 if c.key == key {
  return c.val
 }
 return value(c.Context, key)
}

3. 回溯鏈與樹構建

對 context 包有過了解的都知道,Context 是一種回溯鏈 + 樹形結構構成,爲了方便大家理解,這裏畫一個圖。

context - 樹形. png

圖中虛線代表回溯鏈,實線代表樹形結構。

「回溯鏈:」 context 包中可以通過 WithCancel、WithDeadline、 WithTimeout 或 WithValue 等函數根據祖先 Context 派生出孩子 Context,新派生的 Context 嵌入了祖先 Context 實例,形成了回溯鏈結構(C0~C4),圖中由虛線表示,value 函數就是通過該回溯鏈向上一層層尋找。

「樹形結構:」 cancelCtx 結構體中包含字段 children map[canceler]struct{},可以關聯祖先和孩子節點,Context 樹實質上是一顆 canceler(*cancelCtx 和 *timerCtx)樹(C1、C2、C4 形成一棵樹),map 中只存儲了可取消節點間的父子關係,因爲在級聯取消的時候只需要找到子樹中所有的 canceler 節點(循環 map 中的 key,cancelCtx 小節代碼有使用到),對其進行取消,就可以完成對子樹所有節點生命週期的掌控。

「有同學會有疑問那 valueCtx 這一層如何被取消呢?」 通過對 valueCtx 源碼分析可以知道,valueCtx 並沒有實現非空 Done 方法,其實四種 ctx 實現中只有 cancelCtx 實現了非空 Done 方法,那也就意味着調用 ctx.Done() 會直接轉發到第一個祖先 cancelCtx 上,返回它的 done channel。因此當圖中的 C1 節點取消時,關閉了自身的 done channel,而 C3 節點(valueCtx)中的 done channel 其實就是 C1 節點已關閉的 done channel,因此對其生命週期也進行了管控,其實就是自己的生命週期。

3.1 回溯鏈

回溯鏈是通過嵌入父級 Context 來進行構造的,主要作用有以下兩點:

  1. value() 函數可以沿着回溯鏈向上查找匹配的鍵值對。

  2. 利用 value() 函數邏輯沿着回溯鏈查找最近的  cancelCtx 祖先,用於構造 Context 樹。

3.1.1 回溯鏈的構建

回溯鏈構建與四個主要函數息息相關:「WithCancel、WithDeadline、WithTimeout、WithValue,」 這裏詳細分析一下回溯鏈的構建過程,其他細節(樹構建)後邊討論。

「WithCancel:通過  &cancelCtx{Context: parent}  構建回溯鏈。」

func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
 c := withCancel(parent)
 return c, func() { c.cancel(true, Canceled, nil) }
}

func withCancel(parent Context) *cancelCtx {
 if parent == nil {
  panic("cannot create context from nil parent")
 }
    // 回溯鏈構建
 c := newCancelCtx(parent)
    // 樹構建(後邊詳細介紹)
 propagateCancel(parent, c)
 return c
}

// 回溯鏈構建
func newCancelCtx(parent Context) *cancelCtx {
 return &cancelCtx{Context: parent}
}

「WithDeadline:通過內嵌 cancelCtx 構建回溯鏈。」

func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
 if parent == nil {
  panic("cannot create context from nil parent")
 }
 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,
 }
    // 樹構建(後邊詳細介紹)
 propagateCancel(parent, c)
 dur := time.Until(d)
 if dur <= 0 {
        // 設置的時候就已經改取消了,直接調用取消,返回取消 err,意味着已取消
  c.cancel(true, DeadlineExceeded, nil) // deadline has already passed
        // c.cancel 已經解除與祖先關係,這裏返回的 func 就不需要再解除了
  return c, func() { c.cancel(false, Canceled, nil) }
 }
    // 上鎖
 c.mu.Lock()
 // 解鎖
    defer c.mu.Unlock()
 if c.err == nil {
        // 還未被取消,因爲取消後 err != nil
        // 設置定時器,進行取消
  c.timer = time.AfterFunc(dur, func() {
   c.cancel(true, DeadlineExceeded, nil)
  })
 }
    // 這裏返回的 func 需要解除與祖先的關係
 return c, func() { c.cancel(true, Canceled, nil) }
}

「WithTimeout:複用 WithDeadline 構建回溯鏈。」

func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
 return WithDeadline(parent, time.Now().Add(timeout))
}

「WithValue:通過 &valueCtx{parent, key, val} 構建回溯鏈。」

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

3.1.2 回溯鏈的作用

  1. value() 函數可以沿着回溯鏈向上查找匹配的鍵值對。

  2. 只能找父親節點,不能找兄弟節點

  3. 只能就近取值,如果自己和父母節點都存儲了 key,只能找到自己這裏。

  4. 利用 value() 函數邏輯沿着回溯鏈查找最近的  cancelCtx 祖先,用於構造 Context 樹(這一點需要詳細分析一下 parentCancelCtx 函數的代碼,後續講到樹構建就會一目瞭然)。

func value(c Context, key any) any {
 for {
  switch ctx := c.(type) {
  case *valueCtx:
   if key == ctx.key {
    return ctx.val
   }
   c = ctx.Context
  case *cancelCtx:
   if key == &cancelCtxKey {
    return c
   }
   c = ctx.Context
  case *timerCtx:
   if key == &cancelCtxKey {
    return ctx.cancelCtx
   }
   c = ctx.Context
  case *emptyCtx:
   return nil
  default:
   return c.Value(key)
  }
 }
}

parentCancelCtx:返回 parent 的第一個祖先 cancelCtx 節點。

func parentCancelCtx(parent Context) (*cancelCtx, bool) {
    // 返回第一個實現了 Done() 的實例
 done := parent.Done()
 if done == closedchan || done == nil {
  return nil, false
 }
    // 回溯鏈中尋找第一個 *cancelCtx
 p, ok := parent.Value(&cancelCtxKey).(*cancelCtx)
 if !ok {
  return nil, false
 }
 pdone, _ := p.done.Load().(chan struct{})
    // 判斷回溯鏈中第一個實現 Done() 的實例是不是 cancelCtx 的實例
 if pdone != done {
  return nil, false
 }
 return p, true
}

理解該函數代碼,主要是這一行會有疑惑:if pdone != done,接下來詳細解釋一下: 因爲只有 *cancelCtx 實現了非空 Done 方法,因此 done := parent.Done() 會返回第一個祖先 cancelCtx 中的 done channel,除非該 Context 回溯鏈中存在第三方實現的 Context 接口的實例,parent.Done() 纔有可能返回其他 channel,如下圖所示:假設 C3 是由我們自己實現 Context,parent.Done() 纔會返回自己實現的 Done channel。

context - 第 3 頁. png

理解到這裏,你也就看得懂 parentCancelCtx 函數的代碼了,其實就是返回了 parent 的第一個祖先 cancelCtx 節點。爲啥一定要找到第一個祖先 cancelCtx 節點呢?其實是爲了進行 Context 樹的構建,請看下一小節講解。

3.2 Context 樹

前文提到樹形結構和 children map[canceler]struct{} 字段息息相關,主要作用是關聯祖先節點和孩子節點之間的關係,最終用於在祖先節點取消時,級聯取消所有孩子節點,接下來看一下樹結果是如何構建和進行級聯取消的。

3.2.1 樹構建

Context 樹的構建是在調用 context.WithCancel() 調用時通過 propagateCancel 進行的。調用過程爲:WithCancel -> withCancel -> propagateCancel,具體看一下源碼:

func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
 c := withCancel(parent)
 return c, func() { c.cancel(true, Canceled, nil) }
}

func withCancel(parent Context) *cancelCtx {
 if parent == nil {
  panic("cannot create context from nil parent")
 }
    // 回溯鏈
 c := newCancelCtx(parent)
    // 樹構建
 propagateCancel(parent, c)
 return c
}

func propagateCancel(parent Context, child canceler) {
 done := parent.Done()
 if done == nil {
        // 祖先節點不會被取消,因此不用建立樹結果
  return // parent is never canceled
 }

    // 看看祖先是不是已經取消
 select {
 case <-done:
  // parent is already canceled
  child.cancel(false, parent.Err(), Cause(parent))
  return
 default:
 }

    // 祖先是否是 *cancelCtx 節點
 if p, ok := parentCancelCtx(parent); ok {
  p.mu.Lock()
  if p.err != nil {
   // parent has already been canceled
   child.cancel(false, p.err, p.cause)
  } else {
            // 懶漢式創建
   if p.children == nil {
    p.children = make(map[canceler]struct{})
   }
            // 建立樹結構
   p.children[child] = struct{}{}
  }
  p.mu.Unlock()
 } else {
        // 祖先是第三方實現的 Context 節點
        // 啓動守護線程,在祖先取消時,取消該孩子節點
  goroutines.Add(1)
  go func() {
   select {
   case <-parent.Done():
    child.cancel(false, parent.Err(), Cause(parent))
   case <-child.Done():
   }
  }()
 }
}

代碼註釋寫的很詳細,應該可以看懂,這裏把重點講解一下:done := parent.Done() 沿着回溯鏈找到了第一個實現 Done() 方法的實例,存在已下四種情況:

  1. done channel 爲 nil,表示無法取消,那自然不用級聯取消孩子;

  2. done channel 已經關閉,表示祖先已經取消,自然孩子之間觸發取消就行;

  3. 既然祖先能取消 & 還未被取消,需要判斷祖先是不是 *cancelCtx 節點,利用上一節提到的 parentCancelCtx 函數尋找,如果是  *cancelCtx 則必然實現了 canceler 接口,直接放入 map 中即可;

  4. 祖先不是 *cancelCtx 節點 & 還實現了非空 Done() 函數,那隻能是三方自己實現的類了,因爲不知道其內部實現,所以無法利用 map(有沒有這個字段都不知道),只能開啓一個守護協程,當祖先節點取消時,直接取消孩子節點。

propagateCancel 函數通過處理這四種情況,做到了只要祖先在能取消前提下,發生了取消,必然會觸發孩子節點的取消。又有人會問了,第三種只是塞進去了,沒看到觸發取消呀,這個其實前面已經講過一次了,我們來回顧一下 *cancelCtx 節點的 cancel 方法,也就是觸發級聯取消的地方。

3.2.2 級聯取消

因爲代碼不長,就不做刪減了。代碼中通過 for range map 的方式對其所有的 child 節點做了 cancel,並通過 c.children = nil 的方式斷絕了與所有孩子的聯繫;還通過 parentCancelCtx 函數找到了祖先節點,通過 delete(p.children, child)進行了解綁,斷絕了與祖先的聯繫。

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()
    // err 不爲 nil,表示已經被取消過,那則不用繼續取消了
 if c.err != nil {
        // 解鎖
  c.mu.Unlock()
  return // already canceled
 }
    // 設置取消原因和錯誤
 c.err = err
 c.cause = cause
    // 取出 chan
 d, _ := c.done.Load().(chan struct{})
    // chan 未初始化
 if d == nil {
        // 直接存入已關閉的 chan,通知取消操作
  c.done.Store(closedchan)
 } else {
        // 關閉 chan,通知取消
  close(d)
 }
    // 祖先取消,孩子也得跟着取消(級聯取消)
 for child := range c.children {
  // NOTE: acquiring the child's lock while holding parent's lock.
        // 取消 child
  child.cancel(false, err, cause)
 }
    // 斷絕與所有後代的關係
 c.children = nil
    // 解鎖
 c.mu.Unlock()
 // 如果想斷絕與祖先的關係
 if removeFromParent {
        // 斷絕與祖先的關係
  removeChild(c.Context, c)
 }
}

// removeChild removes a context from its parent.
func removeChild(parent Context, child canceler) {
    // 回溯找到第一個祖先 *cancelCtx
    // 只有 *cancelCtx 中存在 children map[canceler]struct{},纔有父子關係
 p, ok := parentCancelCtx(parent)
    // 沒找到,意味着沒有父子關係,不用斷絕
 if !ok {
  return
 }
 p.mu.Lock()
 if p.children != nil {
        // 祖先的 children map 中移除對該孩子的關係
  delete(p.children, child)
 }
 p.mu.Unlock()
}

總結

閱讀到這裏,context 包的主要內容就串完了,這篇文章只講述了 context 包的源碼,但沒有講解具體如何使用(下一期繼續),迫不及待的同學可以通過 pkg.go.dev 查詢到 context 的官方說明文檔:https://pkg.go.dev/context,裏面記錄了每個對外函數的使用案例。這裏再囉嗦一下 Context 的 「使用規則」

本文詳細的講解了 context 包的源碼,「context」 包主要用於處理併發請求、控制請求執行時間、取消操作、傳遞值等場景。它在處理微服務架構中的請求處理、HTTP 請求處理等方面非常實用。

理解 context 包源碼最重要的是要理解其中的回溯鏈以及樹結構,文章中已經使用詳細的圖進行了講解;回溯鏈中四種實現類 emptyCtx、valueCtx、cancelCtx、timerCtx 完成了不同的功能:

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/NKSKbIO2dV0-s5ix_DI2Ig