【Go进阶—并发编程】Context
作者:与昊
来源:SegmentFault 思否社区
Context 是 Go 应用开发常用的并发控制技术,它与 WaitGroup 最大的不同点是 Context 对于派生 goroutine 有更强的控制力,它可以控制多级的 goroutine。
尽管有很多的争议,但是在很多场景下使用 Context 都很方便,所以现在它已经在 Go 生态圈中传播开来了,包括很多的 Web 应用框架,都切换成了标准库的 Context。标准库中的 database/sql、os/exec、net、net/http 等包中都使用到了 Context。而且,如果遇到了下面的一些场景,也可以考虑使用 Context:
上下文信息传递 ,比如处理 http 请求、在请求处理链路上传递信息;
控制子 goroutine 的运行;
超时控制的方法调用;
可以取消的方法调用。
实现原理
接口定义
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}
context.Background():返回一个非 nil 的、空的 Context,没有任何值,不会被 cancel,不会超时,没有截止日期。一般用在主函数、初始化、测试以及创建根 Context 的时候。
context.TODO():返回一个非 nil 的、空的 Context,没有任何值,不会被 cancel,不会超时,没有截止日期。当你不清楚是否该用 Context,或者目前还不知道要传递一些什么上下文信息的时候,就可以使用这个方法。
一般函数使用 Context 的时候,会把这个参数放在第一个参数的位置。
从来不把 nil 当做 Context 类型的参数值,可以使用 context.Background() 创建一个空的上下文对象,也不要使用 nil。
Context 只用来临时做函数之间的上下文透传,不能持久化 Context 或者把 Context 长久保存。把 Context 持久化到数据库、本地文件或者全局变量、缓存中都是错误的用法。
key 的类型不推荐字符串类型或者其它内建类型,否则容易在包之间使用 Context 时候产生冲突。使用 WithValue 时,key 的类型应该是自己定义的类型。
常常使用 struct{} 作为底层类型定义 key 的类型。对于 exported key 的静态类型,常常是接口或者指针。这样可以尽量减少内存分配。
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
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():
}
}()
}
}
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
}
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) }
}
截止时间到了;
cancel 函数被调用;
parent 的 Done 被 close。
valueCtx
type valueCtx struct {
Context
key, val interface{}
}