Go-context底层原理(1)

60 阅读3分钟

0前言

context 是 golang 中的经典工具,主要在异步场景中用于实现并发协调以及对 goroutine 的生命周期控制. 除此之外,context 还兼有一定的数据存储能力. 本着知其然知其所以然的精神,本文和大家一起深入 context 源码一探究竟,较为细节地对其实现原理进行梳理.

context.Context

Context 为 interface,定义了四个核心 api:

标准error

  • Canceled:context 被 cancel 时会报此错误;
  • DeadlineExceeded:context 超时时会报此错误.

原始父亲类emptyCtx

类的实现

emptyCtx相当于context的初始化鼻祖

各种类返回一个空值,在后面会讲到子context,用以区分。子context都是继承父context从而形成一个新的context。

创建一个父context方法 -> context.Background() & context.TODO()

我们所常用的 context.Background() 和 context.TODO() 方法返回的均是 emptyCtx 类型的一个实例.

子类数据结构 cancelCtx

type cancelCtx struct {
    Context
   
    mu       sync.Mutex            // 用以协调并发场景下的资源获取
    done     atomic.Value          // 实际类型为 chan struct{},即用以反映 cancelCtx 生命周期的通道;
    children map[canceler]struct{} // 保存所有子类context
    err      error                 // 在第一次取消调用时设置为非零
}

type canceler interface {
    cancel(removeFromParent bool, err error)关闭此context.具体实现后面会讲到
    Done() <-chan struct{}//-   当context关闭后Done()返回一个被关闭的管道关闭的管理仍然是可读的据此goroutine可以收到关闭请求
   //如果没有关闭返回nil
}

cancelCtx 未实现Deadline(过期)方法,仅是 embed 了一个带有 Deadline 方法的 Context interface,因此倘若直接调用会报错.

Done方法

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{})
}
  • 基于 atomic 包,读取 cancelCtx 中的 chan;倘若已存在,则直接返回;
  • 加锁后,在此检查 chan 是否存在,若存在则返回;(double check)
  • 初始化 chan 存储到 aotmic.Value 当中,并返回.(懒加载机制)

Err方法

func (c *cancelCtx) Err() error {
    c.mu.Lock() //加锁
    err := c.err//读取cancelCtx.err
    c.mu.Unlock()//解锁
    return err //返回结果
}

Value 方法

通过ctx的key获取value

func (c *cancelCtx) Value(key any) any {
//倘若 key 特定值 &cancelCtxKey,则返回 cancelCtx 自身的指针;
    if key == &cancelCtxKey {
        return c
    }
//否则遵循 valueCtx 的思路取值返回
    return value(c.Context, key)
}

context.WithCancel()为父ctx创建一个子context

func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {

//校验父 context 非空;
    if parent == nil {
        panic("cannot create context from nil parent")
    }
    
//校验父 context 非空;
    c := newCancelCtx(parent)
    
    
//在 propagateCancel 方法内启动一个守护协程,以保证父 context 终止时,该 cancelCtx 也会被终止;
    propagateCancel(parent, &c)
    
    
//将 cancelCtx 返回,连带返回一个用以终止该 cancelCtx 的闭包函数.    
    return &c, func() { c.cancel(true, Canceled) }
}

newCancelCtx为父ctx创建一个新的子ctx

func newCancelCtx(parent Context) cancelCtx {
//注入父 context 后,返回一个新的 cancelCtx.
    return cancelCtx{Context: parent}
}

propagateCancel()用以传递父子 context 之间的 cancel 事件

func propagateCancel(parent Context, child canceler) {
//倘若 parent 是不会被 cancel 的类型(如 emptyCtx),则直接返回;
    done := parent.Done()
    if done == nil {
        return // parent is never canceled
    }

    select {
//倘若 parent 已经被 cancel,则直接终止子 context,并以 parent 的 err 作为子 context 的 err;否则什么都不干
    case <-done:
        // parent is already canceled
        child.cancel(false, parent.Err())
        return
    default:
    }
//假如 parent 是 子ctx 的类型,则加锁,并将子 context 添加到 父ctx 的 children map 当中;
    if p, ok := parentCancelCtx(parent); ok {
        p.mu.Lock()
        if p.err != nil {
            // parent has already been canceled
            child.cancel(false, p.err)
        } else {
            if p.children == nil {
                p.children = make(map[canceler]struct{})
            }
            p.children[child] = struct{}{}
        }
        p.mu.Unlock()
//假如 parent 不是 cancelCtx 类型,但又存在 cancel 的能力(比如用户
//自定义实现的 context),则启动一个协程,通过多路复用的方式监控 parent 状态,倘若其终止,则同时终止子 context,并透传 parent 的 err.
    } else {
        atomic.AddInt32(&goroutines, +1)
        go func() {
            select {
            case <-parent.Done():
                child.cancel(false, parent.Err())
            case <-child.Done():
            }
        }()
    }
}

进一步观察 parentCancelCtx 是如何校验 parent 是否为 子ctx 的类型:

func parentCancelCtx(parent Context) (*cancelCtx, bool) {
    done := parent.Done()
    
    
    ////倘若 parent 的 channel 已关闭或者是不会被 cancel 的类型,则返回 false;
    if done == closedchan || done == nil {
        return nilfalse
    }
    
    
//倘若以特定的 cancelCtxKey 从 parent 中取值,取得的 value 是 parent 本身,则返回 true. (基于 cancelCtxKey 为 key 取值时返回 cancelCtx 自身,是 cancelCtx 特有的协议).    
    p, ok := parent.Value(&cancelCtxKey).(*cancelCtx)
    if !ok {
        return nilfalse
    }
    pdone, _ := p.done.Load().(chan struct{})
    if pdone != done {
        return nilfalse
    }
    return p, true
}