21

golang context源码学习

 3 years ago
source link: https://studygolang.com/articles/30778
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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链可能是这样的:

fuIzeu.png!mobile

当前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取消自己。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK