Golang源码笔记: context包源码阅读

示例

context包也算是golang日常经常用到的包,对于context包的定义是在不同程序之间设置截止时间、取消等信号或者其他请求相关值传递。

func main(){
    // example 1
    ctx,cancel := context.WithTimeout(context.Background(),1*time.Second)
    go doSomething(ctx)
    time.Sleep(2*time.Second)
    cancel()
    fmt.Println("example 1 closed")

    ctx2,cancel2 := context.WithTimeout(context.Background(),1*time.Second)
    go doSomething(ctx2)
    cancel2()
    time.Sleep(1*time.Second)
    fmt.Println("example 2 closed")
}

func doSomething(ctx context.Context){
    select {
    case <-ctx.Done():
        fmt.Println("context done",ctx.Err())
    }
}

输出:

context done context deadline exceeded
example 1 closed
context done context canceled
example 2 closed

示例解析

首先两个例子都设置了超时信号,对应也返回了cancel函数,对于例子1,由于超时信号只有1秒,但是sleep了2秒,所以返回的结果为deadline exceeded,符合预期。
对于例子2,由于设置了1秒超时的上下文之后,立即调用了对应的cancel函数,所以返回了context canceled的结果,预期也是符合的。

源码阅读

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

上述为context包超时的调用,可以看到只是内部调用WithDeadline的函数,没有什么实质性内容,只是将time.Duration作为了time.Time的转换。

func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
    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 {
        c.cancel(true, DeadlineExceeded) // deadline has already passed
        return c, func() { c.cancel(false, Canceled) }
    }
    c.mu.Lock()
    defer c.mu.Unlock()
    if c.err == nil {
        c.timer = time.AfterFunc(dur, func() {
            c.cancel(true, DeadlineExceeded)
        })
    }
    return c, func() { c.cancel(true, Canceled) }
}

对于WithDeadline,由于context可以完成嵌套的调用(在函数参数已经传入一个Context),所以在父级context本身设置了过期时间,并且时间先于当前的d时间参数的情况下,直接返回WithCacel(后面说明)。如果不满足前面提到的条件,即创建一个context,绑定父子关系(后面说明),顺便看下时间是否已经到达,到达则直接调用取消函数(后面说明),否则创建一个定时器,到期调用取消函数。
下面先来看WithCacel的调用以及对应的取消函数,再去看绑定父子关系的propagateCancel函数调用。

// WithCancel returns a copy of parent with a new Done channel. The returned
// context's Done channel is closed when the returned cancel function is called
// or when the parent context's Done channel is closed, whichever happens first.
//
// Canceling this context releases resources associated with it, so code should
// call cancel as soon as the operations running in this Context complete.
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
    c := newCancelCtx(parent)
    propagateCancel(parent, &c)
    return &c, func() { c.cancel(true, Canceled) }
}

withCacel函数本身就是创建一个cancel的context,其实内部包了一层传入的context参数,关联父子上下文之间的关系,具体可以看下文。

// propagateCancel arranges for child to be canceled when parent is.
func propagateCancel(parent Context, child canceler) {
    if parent.Done() == nil {
        return // parent is never canceled
    }
    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()
    } else {
        go func() {
            select {
            case <-parent.Done():
                child.cancel(false, parent.Err())
            case <-child.Done():
            }
        }()
    }
}

propagateCancel函数主要作用如下:

  1. 当父级的Done函数为nil的时候直接返回;
  2. 检测父级是否是可以取消的context,如果是会判断是否已经取消,已取消则子context也取消,否则child加入parent的列表中;
  3. 其他情况下,会启动一个goroutine来监听parent和child的Done channel,如果父级done则调用child的取消函数

最后再看看cacel函数的调用:

// cancel closes c.done, cancels each of c's children, and, if
// removeFromParent is true, removes c from its parent's children.
func (c *cancelCtx) cancel(removeFromParent bool, err error) {
    if err == nil {
        panic("context: internal error: missing cancel error")
    }
    c.mu.Lock()
    if c.err != nil {
        c.mu.Unlock()
        return // already canceled
    }
    c.err = err
    if c.done == nil {
        c.done = closedchan
    } else {
        close(c.done)
    }
    for child := range c.children {
        // NOTE: acquiring the child's lock while holding parent's lock.
        child.cancel(false, err)
    }
    c.children = nil
    c.mu.Unlock()

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

cancel函数的调用很简单,只是将对应的err写入context的err属性,关闭对应的done channel,如果有child列表,则一一调用cancel函数关闭对应的资源。

简要总结

context主要的作用其实是同步信号,由于goroutine创建的树结构的关系,当父节点(指上面的goroutine)关闭了之后,子routine还暂用对应的运算资源,没有同步对应的信号。

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注

15 − 12 =