Go context.WithCancel()源碼剖析

Go context.WithCancel()源碼剖析

Context 接口
type Context interface {
  Deadline() (deadline time.Time, ok bool)
  Done() <-chan struct{}
  Err() error
  Value(key any) any
}
  • Deadline() 上下文的截止時間
  • Done() 上下文是否已關(guān)閉
  • Err() 上下文關(guān)閉的原因
  • Value(key any) 上下文存儲的信息

WithCancel 函數(shù)
type CancelFunc func()

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

這是一個入口函數(shù)规揪,gorutine可以通過該函數(shù)生成一個可cancel的context偷厦。cancel的方法就是該函數(shù)的 cancel CancelFunc 返回體。

可以看到 WithCancel 函數(shù)返回2個對象:

  • Context -> cancelCtx
  • CancelFunc -> func() { c.cancel(true, Canceled, nil) }

其中 Context 實(shí)現(xiàn)為 cancelCtxCancelFunc 為一個 func, func 底層調(diào)用 cancelCtx.cancel()

因此可以看到代碼核心為 cancelCtx 結(jié)構(gòu)體欠雌。


withCancel 函數(shù)
func withCancel(parent Context) *cancelCtx {
  if parent == nil {
    panic("cannot create context from nil parent")
  }
  c := &cancelCtx{}
  c.propagateCancel(parent, c)
  return c
}

代碼邏輯很清晰也很直接策橘,先是初始化一個 cancelCtx 對象锚扎,然后調(diào)用 cancelCtx.propagateCancel(parent, c) 函數(shù)坎拐。

cancelCtx.propagateCancel(parent, c) 實(shí)際上就是綁定兩個context之間的關(guān)系,這個函數(shù)在后面會有詳細(xì)分析芹缔。

因此可以看到 cancelCtx 結(jié)構(gòu)體的核心代碼為 cancelCtx.cancel()cancelCtx.propagateCancel() 這2個函數(shù)坯癣。

接下來我們先分析 cancelCtx 結(jié)構(gòu)體。


cancelCtx結(jié)構(gòu)體
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.mu 為一個操作鎖
  • cancelCtx.done 為一個 atomic.Value 類型的原子容器
  • cancelCtx.children 為一個僅使用key的map

cancelCtx結(jié)構(gòu)體: func (c *cancelCtx) Value(key any) any
// &cancelCtxKey is the key that a cancelCtx returns itself for.
var cancelCtxKey int

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

可以看到 cancelCtx.Value() 函數(shù)的實(shí)現(xiàn)僅為返回 itself 或者 value(parentCtx)

其中 value() 函數(shù)實(shí)現(xiàn)比較簡單最欠,就是不斷往上查找 parentCtx 示罗,直至找到第一個匹配key的映射值


cancelCtx結(jié)構(gòu)體: func (c *cancelCtx) Done() <-chan struct{}
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{})
}

可以看到 cancelCtx.Done() 實(shí)際上是返回一個存儲在 cancelCtx.done 字段內(nèi)的空struct類型的channel通道。

cancelCtx.done 字段的類型為 atomic.Value, 這是一個原子操作存儲對象芝硬。

值得注意的是蚜点,cancelCtx.done 字段初始化使用了"二次校驗(yàn)鎖",這是懶加載保證線程安全的常用對象初始化方式拌阴。


cancelCtx結(jié)構(gòu)體: func (c *cancelCtx) Err() error
func (c *cancelCtx) Err() error {
  c.mu.Lock()
  err := c.err
  c.mu.Unlock()
  return err
}

cancelCtx.Err() 實(shí)現(xiàn)比較簡單绍绘,加鎖拿到上下文關(guān)閉原因,然后返回迟赃。

這里說明陪拘,cancelCtx.err 不為nil則說明 cancelCtx 上下文已關(guān)閉。


cancelCtx結(jié)構(gòu)體: func (c *cancelCtx) propagateCancel(parent Context, child canceler)
func (c *cancelCtx) propagateCancel(parent Context, child canceler) {
  c.Context = parent

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

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

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

cancelCtx.propagateCancel() 代碼核心邏輯是將當(dāng)前 cancelCtx 加入到 parentCtx.children 變量中

  1. 首先 parent.Done() 是否為nil?
- 如果為nil纤壁,那就代表著parent之前所有祖先context都是不能主動關(guān)閉的左刽,無需綁定 `parentCtx` 與 當(dāng)前`cancelCtx` 的映射關(guān)系,邏輯結(jié)束
- 如果不為nil酌媒,則代表 `parentCtx` 是可主動關(guān)閉的欠痴,需要綁定映射關(guān)系
  1. 接著監(jiān)聽 parentCtx 是否剛好關(guān)閉上下文?
- 如果是,代表著當(dāng)前 `cancelCtx` 也得關(guān)閉上下文秒咨,于是通過 `cancelCtx.cancel()` 關(guān)閉喇辽,邏輯結(jié)束
- 如果不是,那么代表著需綁定 `parentCtx` 與 當(dāng)前`cancelCtx` 的映射關(guān)系雨席,邏輯繼續(xù)
  1. 嘗試將 parentCtx 強(qiáng)轉(zhuǎn)為 cancelCtx 類型茵臭,如果強(qiáng)轉(zhuǎn)成功,則加鎖判斷 parentCtx.err 是否為空?
- 為空,則說明 `parentCtx` 上下文尚未關(guān)閉旦委,需要將當(dāng)前 `cancelCtx` 加入到 `parentCtx.children` 變量中,同時邏輯結(jié)束
- 不為空雏亚,則說明其他gorutine在這期間關(guān)閉了上下文缨硝,需要通過 `cancelCtx.cancel()` 關(guān)閉當(dāng)前 `cancelCtx` 上下文,同時邏輯結(jié)束
  1. 嘗試將 parentCtx 強(qiáng)轉(zhuǎn)為 afterFuncer 類型罢低,具體邏輯和 3 類似

  2. 如果 parentCtx 不能強(qiáng)轉(zhuǎn)為 cancelCtxafterFuncer查辩,那么起一個gorutinue監(jiān)聽 parentCtx 和 當(dāng)前 cancelCtx 是否關(guān)閉。

- 這里可能會有點(diǎn)費(fèi)解网持,但是在 `cancelCtx` 初始化期間宜岛,`parentCtx` 是有可能被其他goroutine關(guān)閉上下文的,這里就是預(yù)防這種場景功舀。

cancelCtx結(jié)構(gòu)體: func (c *cancelCtx) cancel(removeFromParent bool, err, cause error)
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 = err
  c.cause = cause
  d, _ := c.done.Load().(chan struct{})
  if d == nil {
    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 = nil
  c.mu.Unlock()

  if removeFromParent {
    removeChild(c.Context, c)
  }
}

cancelCtx.cancelcancelCtx 結(jié)構(gòu)體的核心代碼, 雖然核心但是卻極為簡潔

  1. err 為必選入?yún)ⅲ?code>cause 默認(rèn)等于 err
  2. 加鎖操作萍倡,判斷 cancelCtx.err 是否為空
  • 不為空說明當(dāng)前 cancelCtx 上下文已關(guān)閉,邏輯結(jié)束
  • 為空則說明當(dāng)前 cancelCtx 上下文需要關(guān)閉
  1. 關(guān)閉上下文需要
  • cancelCtx.err孝常,cancelCtx.cause 賦值
  • 關(guān)閉 cancelCtx.done 的channel通道
  • 遍歷并關(guān)閉 cancelCtx.children 的上下文贮竟,最后置空 cancelCtx.children

總結(jié)
  1. cancelCtx 上下文的關(guān)閉信號存儲在 done 字段

  2. cancelCtx.errcancelCtx.causecancelCtx.done 都需要加鎖操作所意,這三個字段都代表著上下文是否已關(guān)閉

  3. 父context如果關(guān)閉,需要主動負(fù)責(zé)關(guān)閉子context

  4. context關(guān)閉是單向傳導(dǎo)姻檀,并不會導(dǎo)致父context關(guān)閉绣版,只會導(dǎo)致所有的子孫context關(guān)閉

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市缩麸,隨后出現(xiàn)的幾起案子杭朱,更是在濱河造成了極大的恐慌八酒,老刑警劉巖羞迷,帶你破解...
    沈念sama閱讀 206,378評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡碍现,警方通過查閱死者的電腦和手機(jī)昼接,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評論 2 382
  • 文/潘曉璐 我一進(jìn)店門铡溪,熙熙樓的掌柜王于貴愁眉苦臉地迎上來棕硫,“玉大人哈扮,你說我怎么就攤上這事包各∥食” “怎么了护姆?”我有些...
    開封第一講書人閱讀 152,702評論 0 342
  • 文/不壞的土叔 我叫張陵须床,是天一觀的道長。 經(jīng)常有香客問我,道長膝捞,這世上最難降的妖魔是什么蔬咬? 我笑而不...
    開封第一講書人閱讀 55,259評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮狐援,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘镶殷。我一直安慰自己,他們只是感情好埋心,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,263評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著腰懂,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上锚沸,一...
    開封第一講書人閱讀 49,036評論 1 285
  • 那天距潘,我揣著相機(jī)與錄音,去河邊找鬼须蜗。 笑死硅确,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的明肮。 我是一名探鬼主播菱农,決...
    沈念sama閱讀 38,349評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼柿估!你這毒婦竟也來了循未?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,979評論 0 259
  • 序言:老撾萬榮一對情侶失蹤秫舌,失蹤者是張志新(化名)和其女友劉穎的妖,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體足陨,經(jīng)...
    沈念sama閱讀 43,469評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡嫂粟,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,938評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了墨缘。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片星虹。...
    茶點(diǎn)故事閱讀 38,059評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡零抬,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出宽涌,到底是詐尸還是另有隱情平夜,我是刑警寧澤,帶...
    沈念sama閱讀 33,703評論 4 323
  • 正文 年R本政府宣布卸亮,位于F島的核電站忽妒,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏兼贸。R本人自食惡果不足惜段直,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,257評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望溶诞。 院中可真熱鬧坷牛,春花似錦、人聲如沸很澄。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽甩苛。三九已至,卻和暖如春俏站,著一層夾襖步出監(jiān)牢的瞬間讯蒲,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工肄扎, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留墨林,地道東北人。 一個月前我還...
    沈念sama閱讀 45,501評論 2 354
  • 正文 我出身青樓犯祠,卻偏偏與公主長得像旭等,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子衡载,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,792評論 2 345

推薦閱讀更多精彩內(nèi)容