关于golang:Go进阶并发编程Context

11次阅读

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

Context 是 Go 利用开发罕用的并发控制技术,它与 WaitGroup 最大的不同点是 Context 对于派生 goroutine 有更强的控制力,它能够管制多级的 goroutine。

只管有很多的争议,然而在很多场景下应用 Context 都很不便,所以当初它曾经在 Go 生态圈中流传开来了,包含很多的 Web 利用框架,都切换成了规范库的 Context。规范库中的 database/sql、os/exec、net、net/http 等包中都应用到了 Context。而且,如果遇到了上面的一些场景,也能够思考应用 Context:

  • 上下文信息传递,比方解决 http 申请、在申请解决链路上传递信息;
  • 管制子 goroutine 的运行;
  • 超时管制的办法调用;
  • 能够勾销的办法调用。

实现原理

接口定义

包 context 定义了 Context 接口,Context 的具体实现包含 4 个办法,别离是 Deadline、Done、Err 和 Value,如下所示:

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

Deadline 办法会返回这个 Context 被勾销的截止日期。如果没有设置截止日期,ok 的值是 false。后续每次调用这个对象的 Deadline 办法时,都会返回和第一次调用雷同的后果。

Done 办法返回一个 Channel 对象,基本上都会在 select 语句中应用。在 Context 被勾销时,此 Channel 会被 close,如果没被勾销,可能会返回 nil。当 Done 被 close 的时候,能够通过 ctx.Err 获取错误信息。Done 这个办法名其实起得并不好,因为名字太过抽象,不能明确反映 Done 被 close 的起因。

对于 Err 办法,你必须要记住的知识点就是:如果 Done 没有被 close,Err 办法返回 nil;如果 Done 被 close,Err 办法会返回 Done 被 close 的起因。

Value 返回此 ctx 中和指定的 key 相关联的 value。

Context 中实现了 2 个罕用的生成顶层 Context 的办法:

  • context.Background():返回一个非 nil 的、空的 Context,没有任何值,不会被 cancel,不会超时,没有截止日期。个别用在主函数、初始化、测试以及创立根 Context 的时候。
  • context.TODO():返回一个非 nil 的、空的 Context,没有任何值,不会被 cancel,不会超时,没有截止日期。当你不分明是否该用 Context,或者目前还不晓得要传递一些什么上下文信息的时候,就能够应用这个办法。

事实上,它们两个底层的实现是截然不同的。绝大多数状况下能够间接应用 context.Background。

在应用 Context 的时候,有一些约定俗成的规定:

  1. 个别函数应用 Context 的时候,会把这个参数放在第一个参数的地位。
  2. 从来不把 nil 当做 Context 类型的参数值,能够应用 context.Background() 创立一个空的上下文对象,也不要应用 nil。
  3. Context 只用来长期做函数之间的上下文透传,不能长久化 Context 或者把 Context 短暂保留。把 Context 长久化到数据库、本地文件或者全局变量、缓存中都是谬误的用法。
  4. key 的类型不举荐字符串类型或者其它内建类型,否则容易在包之间应用 Context 时候产生抵触。应用 WithValue 时,key 的类型应该是本人定义的类型。
  5. 经常应用 struct{} 作为底层类型定义 key 的类型。对于 exported key 的动态类型,经常是接口或者指针。这样能够尽量减少内存调配。

context 包中实现 Context 接口的 struct,除了用于 context.Background() 的 emptyCtx 外,还有 cancelCtx、timerCtx 和 valueCtx 三种。

cancelCtx

type cancelCtx struct {
    Context

    mu       sync.Mutex            // 互斥锁
    done     atomic.Value          // 调用 cancel 时会敞开的 channel
    children map[canceler]struct{} // 记录了由此 Context 派生的所有 child,此 Context 被 cancel 时会同时 cancel 所有 child
    err      error                 // 错误信息
}
WithCancel

cancelCtx 是通过 WithCancel 办法生成的。咱们经常在一些须要被动勾销长时间的工作时,创立这种类型的 Context,而后把这个 Context 传给长时间执行工作的 goroutine。当须要停止工作时,咱们就能够 cancel 这个 Context,这样长时间执行工作的 goroutine,就能够通过查看这个 Context,晓得 Context 曾经被勾销了。

WithCancel 返回值中的第二个值是一个 cancel 函数。记住,不是只有你想中途放弃,才去调用 cancel,只有你的工作失常实现了,就须要调用 cancel,这样,这个 Context 能力开释它的资源(告诉它的 children 解决 cancel,从它的 parent 中把本人移除,甚至开释相干的 goroutine)。

看下外围源码:

func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
    if parent == nil {panic("cannot create context from nil parent")
    }
    c := newCancelCtx(parent)
    propagateCancel(parent, &c)
    return &c, func() { c.cancel(true, Canceled) }
}

func newCancelCtx(parent Context) cancelCtx {return cancelCtx{Context: parent}
}

func propagateCancel(parent Context, child canceler) {done := parent.Done()

    if p, ok := parentCancelCtx(parent); ok {p.mu.Lock()
        if p.err != nil {
            // parent 曾经勾销了,间接勾销子 Context
            child.cancel(false, p.err)
        } else {
            // 将 child 增加到 parent 的 children 切片
            if p.children == nil {p.children = make(map[canceler]struct{})
            }
            p.children[child] = struct{}{}
        }
        p.mu.Unlock()} else {atomic.AddInt32(&goroutines, +1)
        // 没有 parent 能够“挂载”,启动一个 goroutine 监听 parent 的 cancel,同时 cancel 本身
        go func() {
            select {case <-parent.Done():
                child.cancel(false, parent.Err())
            case <-child.Done():}
        }()}
}

代码中调用的 propagateCancel 办法会顺着 parent 门路往上找,直到找到一个 cancelCtx,或者为 nil。如果不为空,就把本人退出到这个 cancelCtx 的 child,以便这个 cancelCtx 被勾销的时候告诉本人。如果为空,会新起一个 goroutine,由它来监听 parent 的 Done 是否已敞开。

当这个 cancelCtx 的 cancel 函数被调用的时候,或者 parent 的 Done 被 close 的时候,这个 cancelCtx 的 Done 才会被 close。

cancel 是向下传递的,如果一个 WithCancel 生成的 Context 被 cancel 时,如果它的子 Context(也有可能是孙,或者更低,依赖子的类型)也是 cancelCtx 类型的,就会被 cancel。

cancel

cancel 办法的作用是 close 本人及其后辈的 done 通道,达到告诉勾销的目标。WithCancel 办法的第二个返回值 cancel 就是本函数。来看一下次要代码实现:

func (c *cancelCtx) cancel(removeFromParent bool, err error) {c.mu.Lock()
    // 设置 cancel 的起因
    c.err = err 
    // 敞开本身的 done 通道
    d, _ := c.done.Load().(chan struct{})
    if d == nil {c.done.Store(closedchan)
    } else {close(d)
    }
    // 遍历所有 children,一一调用 cancel 办法
    for child := range c.children {child.cancel(false, err)
    }
    c.children = nil
    c.mu.Unlock()

    // 失常状况下,须要将本身从 parent 的 children 切片中删除
    if removeFromParent {removeChild(c.Context, c)
    }
}

timerCtx

type timerCtx struct {
    cancelCtx
    timer *time.Timer 

    deadline time.Time
}

timerCtx 在 cancelCtx 根底上减少了 deadline 用于标示主动 cancel 的最终工夫,而 timer 就是一个触发主动 cancel 的定时器。timerCtx 能够由 WithDeadline 和 WithTimeout 生成,WithTimeout 理论调用了 WithDeadline,二者实现原理统一,只不过应用语境不一样:WithDeadline 是指定最初期限,WithTimeout 是指定最长存活工夫。

WithDeadline

来看一下 WithDeadline 办法的实现:

func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
    // 如果 parent 的截止工夫更早,间接返回一个 cancelCtx 即可
    if cur, ok := parent.Deadline(); ok && cur.Before(d) {return WithCancel(parent)
    }
    c := &timerCtx{cancelCtx: newCancelCtx(parent),
        deadline:  d,
    }
    propagateCancel(parent, c) // 同 cancelCtx 的解决逻辑
    dur := time.Until(d)
    if dur <= 0 { // 以后工夫曾经超过了截止工夫,间接 cancel
        c.cancel(true, DeadlineExceeded)
        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 会返回一个 parent 的正本,并且设置了一个不晚于参数 d 的截止工夫,类型为 timerCtx(或者是 cancelCtx)。

如果它的截止工夫晚于 parent 的截止工夫,那么就以 parent 的截止工夫为准,并返回一个类型为 cancelCtx 的 Context,因为 parent 的截止工夫到了,就会勾销这个 cancelCtx。如果以后工夫曾经超过了截止工夫,就间接返回一个曾经被 cancel 的 timerCtx。否则就会启动一个定时器,到截止工夫勾销这个 timerCtx。

综合起来,timerCtx 的 Done 被 Close 掉,次要是由上面的某个事件触发的:

  • 截止工夫到了;
  • cancel 函数被调用;
  • parent 的 Done 被 close。

和 cancelCtx 一样,WithDeadline(WithTimeout)返回的 cancel 肯定要调用,并且要尽可能早地被调用,这样能力尽早开释资源,不要单纯地依赖截止工夫被动勾销。

valueCtx

type valueCtx struct {
    Context
    key, val interface{}}

valueCtx 只是在 Context 根底上减少了一个 key-value 对,用于在各级协程间传递一些数据。

WithValue 基于 parent Context 生成一个新的 valueCtx,保留了一个 key-value 键值对。valueCtx 笼罩了 Value 办法,优先从本人的存储中查看这个 key,不存在的话会从 parent 中持续查看。

正文完
 0