Golang context是Golang应用开发常用的并发控制技术,它与WaitGroup最大的不同点是context对于派生goroutine有更强的控制力,它可以控制多级的goroutine。
context翻译成中文是”上下文”,即它可以控制一组呈树状结构的goroutine,每个goroutine拥有相同的上下文。
典型的使用场景如下图所示:
上图中由于goroutine派生出子goroutine,而子goroutine又继续派生新的goroutine,这种情况下使用WaitGroup就不太容易,因为子goroutine个数不容易确定。而使用context就可以很容易实现。
1. Context实现原理
context实际上只定义了接口,凡是实现该接口的类都可称为是一种context,官方包中实现了几个常用的context,分别可用于不同的场景
type Context interface {
// Deadline 返回 context 被取消的时间和一个 bool 值,表示是否设置了截止时间。
Deadline() (deadline time.Time, ok bool)
// Done 返回一个 channel,当 context 被取消或截止时间到达时,该 channel 会关闭。
Done() <-chan struct{}
// Err 返回 context 被取消的原因。
// 如果 Done 返回的 channel 关闭了,Err 返回一个非空的错误。
Err() error
// Value 返回与 key 关联的值,或者 nil 如果没有找到。
Value(key interface{}) interface{}
}
基础的context接口只定义了4个方法,下面分别简要说明一下:
1.1 Deadline()
该方法返回一个deadline和标识是否已设置deadline的bool值,如果没有设置deadline,则ok == false,此时deadline为一个初始值的time.Time值
1.2 Done()
该方法返回一个channel,需要在select-case语句中使用,如”case <-context.Done():”。
当context关闭后,Done()返回一个被关闭的管道,关闭的管理仍然是可读的,据此goroutine可以收到关闭请求;当context还未关闭时,Done()返回nil。
1.3 Err()
该方法描述context关闭的原因。关闭原因由context实现控制,不需要用户设置。比如Deadline context,关闭原因可能是因为deadline,也可能提前被主动关闭,那么关闭原因就会不同:
- 因deadline关闭:“context deadline exceeded”;
- 因主动关闭: “context canceled”。
当context关闭后,Err()返回context的关闭原因;当context还未关闭时,Err()返回nil;
1.4 Value()
有一种context,它不是用于控制呈树状分布的goroutine,而是用于在树状分布的goroutine间传递信息。
Value()方法就是用于此种类型的context,该方法根据key值查询map中的value。
2. 空context
context包中定义了一个空的context,名为emptyCtx,用于context的根节点,空的context只是简单的实现 了Context,本身不包含任何值,仅用于其他context的父节点。可以通过 context.Background()
获取
context包提供了4个方法创建不同类型的context,这四个方法使用时如果没有父context就可以传入backgroud
- WithCancel()
- WithDeadline()
- WithTimeout()
- WithValue()
context包中实现Context接口的struct,除了emptyCtx外,还有cancelCtx、timerCtx和valueCtx三种,正 是基于这三种context实例,实现了上述4种类型的context。
3. cancelCtx
context.WithCancel(parent)
-
创建一个新的
Context
,其生命周期可通过调用取消函数cancel()
来结束 -
当
cancel()
被调用时,所有从这个Context
派生的Context
都会被取消
type cancelCtx struct {
Context
mu sync.Mutex // 保护以下字段
done chan struct{} // 延迟创建,第一次取消调用时关闭
err error // 第一次取消调用时设置为非 nil
children map[canceler]struct{} // 第一次取消调用时设置为 nil
}
children中记录了由此context派生的所有child,此context被cancle时会把其中的所有child都cancle掉。
cancelCtx与deadline和value无关,所以只需要实现Done()和Err()接口外露接口即可。
4. timerCtx
context.WithTimeout(parent, timeout)
-
创建一个
Context
,带有自动取消的截止时间 -
超过指定的
timeout
后,Context
会自动取消
type timerCtx struct {
cancelCtx
timer *time.Timer
deadline time.Time
}
timerCtx在cancelCtx基础上增加了deadline用于标示自动cancel的最终时间,而timer就是一个触发自动cancel的定时器。
由此,衍生出WithDeadline()和WithTimeout()。实现上这两种类型实现原理一样,只不过使用语境不一样:
- deadline: 指定最后期限,比如context将2024.08.16 00:00:00之时自动结束
- timeout: 指定最长存活时间,比如context将在30s后结束
对于接口来说,timerCtx在cancelCtx基础上还需要实现Deadline()和cancel()方法,其中cancel()方法是重写的。
5. valueCtx
context.WithValue(parent, key, value)
创建一个从父 Context
派生的新 Context
,可以在其中存储键值对
仅用于传递请求范围的信息,不应用于传递函数参数
type valueCtx struct {
Context
key, val interface{}
}
valueCtx在Context基础上增加了一个key-value对,用于在各级协程间传递一些数据。
6.总结
- Context仅仅是一个接口定义,跟据实现的不同,可以衍生出不同的context类型;
- cancelCtx实现了Context接口,通过WithCancel()创建cancelCtx实例;
- timerCtx实现了Context接口,通过WithDeadline()和WithTimeout()创建timerCtx实例;
- valueCtx实现了Context接口,通过WithValue()创建valueCtx实例;
- 三种context实例可互为父节点,从而可以组合成不同的应用形式;