关于golang:golang-context源码学习

7次阅读

共计 8522 个字符,预计需要花费 22 分钟才能阅读完成。

golang context 源码学习

应用实例

context 设置超时的例子

func main() {ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
    defer cancel()

    go handle(ctx, 1500*time.Millisecond)
    select {case <-ctx.Done():
        fmt.Println("main", ctx.Err())
    }

    time.Sleep(3 * time.Second)
}

func handle(ctx context.Context, duration time.Duration) {
    select {case <-ctx.Done():
        fmt.Println("handle", ctx.Err())
    case <-time.After(duration):
        fmt.Println("process request with", duration)
    }
}

输入:

main context deadline exceeded
handle context deadline exceeded

Context 设置的超时工夫是 1s,然而解决工夫是 1.5s,最初必定会触发超时,在 handle 协程里,time.After 会在程序启动 1.5s 之后返回音讯,而 ctx.Done()会在 1s 当前返回音讯,time.After 没有机会被捕捉到,handle 协程就退出了;而主协程同样也能捕捉到 ctx.Done 里的音讯。

context 的应用办法和设计原理 多个 Goroutine 同时订阅 ctx.Done() 管道中的音讯,一旦接管到勾销信号就立即进行以后正在执行的工作。

源码解析

context.Context 接口

type Context interface{Deadline() (deadline time.Time, ok bool)
    Done() <-chan struct{}
    Err() error
    Value(key interface{}) interface{}}

Context 接口定了四个办法:

  • Deadline()会返回这个 context 终止时的工夫以及是否被设置了超时。
  • Done()会返回一个只读管道,通常须要内部有 select 语句来监听这个管道,若是 context 被 cancel 掉,那么通过该接口可能获取到音讯,没被 cancel 时,会始终阻塞在 context.Done()的读取上。这里的 cancel 指 WithCancel,WithDeadline,WithTimeout 触发的 cancel。
  • 若 ctx 没被 cancel 掉,Err()只会返回 nil,若被 cancel 掉则会返回为何被 cancel,例如 deadline。
  • Value()接口是用来存值的,在后续的 context 中能够将其取出。

context.Background(),context.TODO()

context.Background()和 context.TODO()会返回一个雷同的构造体,即 emptyCtx。

var (background = new(emptyCtx)
    todo       = new(emptyCtx)
)

emptyCtx 对 Context 接口的办法实现如下:

// An emptyCtx is never canceled, has no values, and has no deadline. It is not
// struct{}, since vars of this type must have distinct addresses.
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 interface{}) interface{} {return nil}

即它没有对 Context 进行任何的实现。

  • Background()返回的 context 通常作为整个 context 调用链的根 context。
  • TODO()返回的 context 通常是在重构或编码过程中应用,不确定会如何应用

Context 参数,但用其作为占位符,TODO 的标识便于后续代码实现时能疾速查看到这个未实现的占位符,以便将其实现。

context.WithCancel()

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

从函数签名能够看出,WithCancel 会返回一个设置了 cancel 的 context 和一个勾销函数,这个返回的 context 的 Done 管道会被敞开,当父 context 的 Done 管道被敞开时或者勾销函数被调用时。

创立一个 cancelCtx

c := newCancelCtx(parent)
// newCancelCtx returns an initialized cancelCtx.
func newCancelCtx(parent Context) cancelCtx {return cancelCtx{Context: parent}
}

看一下 cancelCtx 的构造:

// A cancelCtx can be canceled. When canceled, it also cancels any children
// that implement canceler.
type cancelCtx struct {
    Context

    mu       sync.Mutex            // protects following fields
    done     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
}

cancelCtx 勾销时,会将后辈节点中所有的 cancelCtx 都勾销,propagateCancel 即用来建设以后节点与先人节点这个勾销关联逻辑。

propagateCancel(parent, &c)

为啥须要建设与先人的关联逻辑呢?后续会提到。

留神,propagateCancel 的第二个参数是一个 canceler 接口,由定义可知:

// A canceler is a context type that can be canceled directly. The
// implementations are *cancelCtx and *timerCtx.
type canceler interface {cancel(removeFromParent bool, err error)
    Done() <-chan struct{}
}

这个接口由两种带有勾销性质的 ctx 实现(cancelCtx 和 timerCtx)

  • 第一个办法是 cancel,由参数能够知它会将本身勾销掉,若第一个参数为 true,则会从父 ctx 中删除掉本人
  • 第二个办法会间接返回一个 Done 管道
// 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} // 如果 parent 的 Done 管道是空,阐明该父 context 永远不会被勾销,通常是 emptyCtx,那么不必建设该 ctx 与先人 ctx 的关系

    if p, ok := parentCancelCtx(parent); ok {// 当其父 context 为 cancel 性质的 context(timerCtx 或 cancelCtx)会返回 true 和这个 cancelCtx,若为 valueCtx 则会由 context 向上查找直到找到一个 cancel 性质的 ctx;否则返回 false

        // 父 ctx 为 cancel 性质的 ctx

          // 加锁
        p.mu.Lock()
        if p.err != nil {
            // parent has already been canceled
              // 父 ctx 曾经被 cancel 掉了,所以该子 ctx 将会间接调用 cancel 办法 cancel 掉本人。// 这样,尽管之后给内部调用返回了一个 cancel 函数,然而因为在 child.cancel 中曾经设置了 c.err,所以之后内部再调用 cancel,cancelCtx 的 cancel 办法也不会再做别的操作了,发现 c.err 不为 nil,间接 return,代表曾经被 cancel 掉了
            child.cancel(false, p.err)
        } else {
              // 否则把本人退出到先人 cancelCtx 的 children 中
            if p.children == nil {p.children = make(map[canceler]struct{})
            }
            p.children[child] = struct{}{}
        }
        p.mu.Unlock()} else {
        // 父 context 不为 cancelCtx,则起一个后盾协程,监听父 ctx 和以后 ctx 的 Done 管道,直到有 ctx 的 Done 管道被敞开(ctx 被勾销)//TODO,为何 parent 不为 cancelCtx,还能有 done 信号被捕捉到???go func() {
            select {case <-parent.Done():
                   // 这种状况何时会呈现?child.cancel(false, parent.Err())
            case <-child.Done():}
        }()}
}
  1. 如果 parent.Done()返回 nil,表明父节点以上的门路上没有可勾销的 context,不须要解决;
  2. 如果在 context 链上找到到 cancelCtx 类型的先人节点,则判断这个先人节点是否曾经勾销,如果曾经勾销就勾销以后节点;否则将以后节点退出到先人节点的 children 列表。

否则开启一个协程,监听 parent.Done()和 child.Done(),一旦 parent.Done()返回的 channel 敞开,即 context 链中某个先人节点 context 被勾销,则将以后 context 也勾销。
这里或者有个疑难,为什么是先人节点而不是父节点?这是因为以后 context 链可能是这样的:

以后 cancelCtx 的父节点 context 并不是一个可勾销的 context,也就没法记录 children。这也是为什么须要在这个函数中建设以后节点与先人 cancelCtx 节点的 cancel 关系

问题:
为什么会有

go func() {
            select {case <-parent.Done():
                   // 这种状况何时会呈现?child.cancel(false, parent.Err())
            case <-child.Done():}
        }()

因为 else 中,曾经阐明了先人 ctx 不为可勾销的 ctx,那为啥还可能捕捉到第一个 case 的 Done 管道的信号呢?

这里须要引入 parentCancelCtx 的代码

func parentCancelCtx(parent Context) (*cancelCtx, bool) {
    for {switch c := parent.(type) {
        case *cancelCtx:
            return c, true
        case *timerCtx:
            return &c.cancelCtx, true
        case *valueCtx:
            parent = c.Context
        default:
            return nil, false
        }
    }
}

这里只能判断出 cancelCtx,timeCtx,valueCtx 三种类型,而若是将 ctx 内嵌到一个自定义的构造体 a 中,并且之后调用了 WithCancel 构建了子节点 b。将这个子节点再调用 WithCancel 构建孙节点 c,此时 parentCancelCtx 办法是辨认不出这个 b 为 cancelCtx 的,因而须要应用 else 下的第一个 case 来捕捉 b 节点的 Done 音讯。

再来说一下,select 语句里的两个 case 其实都不能删。

select {case <-parent.Done():
        child.cancel(false, parent.Err())
    case <-child.Done():}
  • 第一个 case 阐明当父节点勾销,则勾销子节点。如果去掉这个 case,那么父节点勾销的信号就不能传递到子节点。
  • 第二个 case 是说如果子节点本人勾销了,那就退出这个 select,父节点的勾销信号就不必管了。如果去掉这个 case,那么很可能父节点始终不勾销,这个 goroutine 就透露了。当然,如果父节点勾销了,就会反复让子节点勾销,不过,这也没什么影响嘛。

返回一个 cancelCtx 与一个 cancel 函数

return &c, func() { c.cancel(true, Canceled) }

这个 cancel 函数被内部调用时,会将本身从父节点中删除掉。并且 cancel 掉该节点的所有子节点:

// 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 {
          // 惰性创立,初始化时不会赋值。须要勾销时间接给一个敞开的 channel
          // 很有意思的是这个 channel 是在 init 里被敞开的
        c.done = closedchan
    } else {close(c.done)
    }

    //cancel 掉该节点的所有子节点
    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()

      // 何时该参数应该为 true?// 当内部调用 cancel 时,该参数为 true
    if removeFromParent {removeChild(c.Context, c)
    }
}

两个问题须要答复:

  1. 什么时候会传 true?
  2. 为什么有时传 true,有时传 false?

答 1:
内部调用 cancel 时传 true,外部调用 cancel 时传 false。
调用 WithCancel() 办法的时候,也就是新创建一个可勾销的 context 节点时,返回的 cancelFunc 函数会传入 true。这样做的后果是:当调用返回的 cancelFunc 时,会将这个 context 从它的父节点里“除名”,因为父节点可能有很多子节点,你本人勾销了,所以我要和你断绝关系,对其他人没影响

答 2:
外部调用 cancel 时不必从父节点删除掉本身。外部调用的机会是:

1. 在建设先人与以后 ctx 的 cancel 关系时,若发现先人曾经被 cancel 了,这时会外部调用 cancel:这种状况下,先人通常是曾经被内部调用了 cancel,它曾经将其 chidren 置为了 nil,这时就不必要再删除了;2. 监听到先人的 Done 管道敞开,即先人曾经被 cancel 掉,这种状况和第一种状况相似,children 曾经被置为了 nil,不必要再删除。

context.WithTimeout()

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

context.WithTimeout 办法底层其实是调用的 WithDeadline,返回了一个带有 超时信息的 context 和一个勾销函数,规范用法如下:

func slowOperationWithTimeout(ctx context.Context) (Result, error) {ctx, cancel := context.WithTimeout(ctx, 100*time.Millisecond)
    defer cancel()  // releases resources if slowOperation completes before timeout elapses
    return slowOperation(ctx)
}

看一下 WithDeadline 的实现形式

func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)

由函数签名能够看出会返回一个带超时的 context 和一个勾销函数,若 WithDeadline 返回的 context 在 d 工夫点未被勾销,那么它的 Done 管道将被强制敞开。

具体实现代码:

判断父 context 是否曾经超时了:

if cur, ok := parent.Deadline(); ok && cur.Before(d) {
        // The current deadline is already sooner than the new one.
        return WithCancel(parent)
    }

若是父 context 被设置了超时,且截止工夫点要短于以后冀望设置的超时工夫的截止工夫点,那么间接基于父 ctx 构建一个可勾销的 ctx。
起因是一旦父节点超时,主动调用 cancel 函数,子节点也会随之勾销。因而构建一个相对工夫晚于父节点的子 ctx 是没有意义的。

构建 timerCtx

首先须要理解一下 timerCtx:

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

能够看到 timerCtx 内嵌了 cancelCtx,同时它还有本人的 timer 和 deadline。
Timer 是一个定时器,当达到设定的工夫后,会向 timer 的管道中发一个事件。
会在 deadline 到来时,会监听到事件,这时主动勾销 context,这是在 ctx 的 WithDeadline 中做的。

看下 timerCtx 的 cancel 办法

func (c *timerCtx) cancel(removeFromParent bool, err error) {c.cancelCtx.cancel(false, err)
    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()}
  1. 首先会将本人和所有子节点 cancel 掉;
  2. 会将本身的 timer 的 Stop 掉,避免 deadline 到来时再次被勾销。

构建以后 ctx 与先人 ctx 的 cancel 关系

propagateCancel(parent, c)

这个与之前的 cancel 是一样的,不再赘述。

计算以后工夫与 deadline 的时间差

dur := time.Until(d)
    if dur <= 0 {c.cancel(true, DeadlineExceeded) // deadline has already passed
        return c, func() { c.cancel(false, Canceled) }
    }

如果以后工夫曾经达到了 deadline 的工夫点,那么间接将其勾销,并返回。

设置 timer 到期的回调函数

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

如果 timer 到期后 timerCtx 会调用 cancel 勾销本人。

参考文章

https://www.zhihu.com/search?…

正文完
 0