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() 上下文是否已关闭
  • Err() 上下文关闭的原因
  • Value(key any) 上下文存储的信息

WithCancel 函数
type CancelFunc func()

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

这是一个入口函数,gorutine可以通过该函数生成一个可cancel的context。cancel的方法就是该函数的 cancel CancelFunc 返回体。

可以看到 WithCancel 函数返回2个对象:

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

其中 Context 实现为 cancelCtxCancelFunc 为一个 func, func 底层调用 cancelCtx.cancel()

因此可以看到代码核心为 cancelCtx 结构体。


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

代码逻辑很清晰也很直接,先是初始化一个 cancelCtx 对象,然后调用 cancelCtx.propagateCancel(parent, c) 函数。

cancelCtx.propagateCancel(parent, c) 实际上就是绑定两个context之间的关系,这个函数在后面会有详细分析。

因此可以看到 cancelCtx 结构体的核心代码为 cancelCtx.cancel()cancelCtx.propagateCancel() 这2个函数。

接下来我们先分析 cancelCtx 结构体。


cancelCtx结构体
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结构体: 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() 函数的实现仅为返回 itself 或者 value(parentCtx)

其中 value() 函数实现比较简单,就是不断往上查找 parentCtx ,直至找到第一个匹配key的映射值


cancelCtx结构体: 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() 实际上是返回一个存储在 cancelCtx.done 字段内的空struct类型的channel通道。

cancelCtx.done 字段的类型为 atomic.Value, 这是一个原子操作存储对象。

值得注意的是,cancelCtx.done 字段初始化使用了"二次校验锁",这是懒加载保证线程安全的常用对象初始化方式。


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

cancelCtx.Err() 实现比较简单,加锁拿到上下文关闭原因,然后返回。

这里说明,cancelCtx.err 不为nil则说明 cancelCtx 上下文已关闭。


cancelCtx结构体: 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() 代码核心逻辑是将当前 cancelCtx 加入到 parentCtx.children 变量中

  1. 首先 parent.Done() 是否为nil?
- 如果为nil,那就代表着parent之前所有祖先context都是不能主动关闭的,无需绑定 `parentCtx` 与 当前`cancelCtx` 的映射关系,逻辑结束
- 如果不为nil,则代表 `parentCtx` 是可主动关闭的,需要绑定映射关系
  1. 接着监听 parentCtx 是否刚好关闭上下文?
- 如果是,代表着当前 `cancelCtx` 也得关闭上下文,于是通过 `cancelCtx.cancel()` 关闭,逻辑结束
- 如果不是,那么代表着需绑定 `parentCtx` 与 当前`cancelCtx` 的映射关系,逻辑继续
  1. 尝试将 parentCtx 强转为 cancelCtx 类型,如果强转成功,则加锁判断 parentCtx.err 是否为空?
- 为空,则说明 `parentCtx` 上下文尚未关闭,需要将当前 `cancelCtx` 加入到 `parentCtx.children` 变量中,同时逻辑结束
- 不为空,则说明其他gorutine在这期间关闭了上下文,需要通过 `cancelCtx.cancel()` 关闭当前 `cancelCtx` 上下文,同时逻辑结束
  1. 尝试将 parentCtx 强转为 afterFuncer 类型,具体逻辑和 3 类似

  2. 如果 parentCtx 不能强转为 cancelCtxafterFuncer,那么起一个gorutinue监听 parentCtx 和 当前 cancelCtx 是否关闭。

- 这里可能会有点费解,但是在 `cancelCtx` 初始化期间,`parentCtx` 是有可能被其他goroutine关闭上下文的,这里就是预防这种场景。

cancelCtx结构体: 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 结构体的核心代码, 虽然核心但是却极为简洁

  1. err 为必选入参,cause 默认等于 err
  2. 加锁操作,判断 cancelCtx.err 是否为空
  • 不为空说明当前 cancelCtx 上下文已关闭,逻辑结束
  • 为空则说明当前 cancelCtx 上下文需要关闭
  1. 关闭上下文需要
  • cancelCtx.errcancelCtx.cause 赋值
  • 关闭 cancelCtx.done 的channel通道
  • 遍历并关闭 cancelCtx.children 的上下文,最后置空 cancelCtx.children

总结
  1. cancelCtx 上下文的关闭信号存储在 done 字段

  2. cancelCtx.errcancelCtx.causecancelCtx.done 都需要加锁操作,这三个字段都代表着上下文是否已关闭

  3. 父context如果关闭,需要主动负责关闭子context

  4. context关闭是单向传导,并不会导致父context关闭,只会导致所有的子孙context关闭

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,098评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,213评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,960评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,519评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,512评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,533评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,914评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,804评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,563评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,644评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,350评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,933评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,908评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,146评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,847评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,361评论 2 342

推荐阅读更多精彩内容