定义
context,中文译作“上下文”,准确说它是 goroutine 的上下文,包含 goroutine 的运行状态、环境、现场等信息。是一种并发安全在 goroutine 之间传递上下文信息,包括:取消信号、超时时间、截止时间、k-v 等的类型。
使用场景
context用来解决goroutine之前退出通知、元数据传递的功能的问题。
定时取消
可以通过WithDealine()和WithTimeout()来获得一个到达某个时间点取消当前goroutine以及子goroutine的context;当然也可以在上层调用返回的cancel方法取消子goroutine。
package main
func main(){
ctx,cancel := WithTimeout(context.Background(),1*time.Second)
defer cancel()
res := process(ctx)
fmt.Println(res)
}
func process(ctx context.Context)int{
time.Sleep(3*time.Second)
return 1
}
防止goroutine泄露
前面那个例子里,goroutine 还是会自己执行完,最后返回,只不过会多浪费一些系统资源。这里改编一个“如果不用 context 取消,goroutine 就会泄漏的例子”。
func main() {
for n := range gen() {
fmt.Println(n)
if n == 5 {
break
}
}
// ……
}
func gen() <-chan int {
ch := make(chan int)
go func() {
var n int
for {
ch <- n
n++
time.Sleep(time.Second)
}
}()
return ch
}
gen是一个可以生成无限整数的协程,但如果我只需要它产生的前 5 个数,那么就会发生 goroutine 泄漏。当 n == 5 的时候,直接 break 掉。那么 gen 函数的协程就会执行无限循环,永远不会停下来。发生了 goroutine 泄漏。
使用context优化:
func gen(ctx context.Context) <-chan int {
ch := make(chan int)
go func() {
var n int
for {
select {
case <-ctx.Done():
return
case ch <- n:
n++
time.Sleep(time.Second)
}
}
}()
return ch
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel() // 避免其他地方忘记 cancel,且重复调用不影响
for n := range gen(ctx) {
fmt.Println(n)
if n == 5 {
cancel()
break
}
}
// ……
}
增加一个 context,在 break 前调用 cancel 函数,取消 goroutine。gen 函数在接收到取消信号后,直接退出,系统回收资源。
传递共享数据
使用WithValue()可以往返回一个指协程,并且写入了一对key-value。
package main
import (
"context"
"fmt"
)
func main() {
ctx := context.Background()
process(ctx)
ctx = context.WithValue(ctx, "traceId", "qcrao-2019")
process(ctx)
}
func process(ctx context.Context) {
traceId, ok := ctx.Value("traceId").(string)
if ok {
fmt.Printf("process over. trace_id=%s\n", traceId)
} else {
fmt.Printf("process over. no trace_id\n")
}
}
注意事项
- 不要将 Context 塞到结构体里。直接将 Context 类型作为函数的第一参数,而且一般都命名为 ctx。
- 不要向函数传入一个 nil 的 context,如果你实在不知道传什么,标准库给你准备好了一个 context:todo。
- 不要把本应该作为函数参数的类型塞到 context 中,context 存储的应该是一些共同的数据。例如:登陆的 session、cookie 等。
- 同一个 context 可能会被传递到多个 goroutine,别担心,context 是并发安全的。
context底层原理
context是一个官方包,包中Context是一个接口包含Deadline(),Done(),Err(),value()四个方法;还有一个canceler接口,包含cancel()和Done()2个方法。cancelCtx结构体实现了canceler接口,可以通过WithCancel()方法返回一个cancelCtx对象和取消方法,调用取消方法可以关闭该channel的done channel。timerCtx结构体是基于cancelCtx的,只是多了一个time.Timer和一个deadline,WithDeadline()返回的conctext在未来什么时候会取消,WhithTimeout()调用了WithDeadline(),会把当前时间加上时间间隔。valueCtx也是一个结构体,有key和value两个字段,通过WithValue()方法可以创建一个context存放了key-value,且父节点是传入的context。查找context过程就是从子节点往上查找,直到key匹配上。
context接口
type Context interface {
// 当 context 被取消或者到了 deadline,返回一个被关闭的 channel
Done() <-chan struct{}
// 在 channel Done 关闭后,返回 context 取消原因
Err() error
// 返回 context 是否会被取消以及自动取消时间(即 deadline)
Deadline() (deadline time.Time, ok bool)
// 获取 key 对应的 value
Value(key interface{}) interface{}
}
Context 是一个接口,定义了 4 个方法,它们都是幂等的。也就是说连续多次调用同一个方法,得到的结果都是相同的。
Done() 返回一个 channel,可以表示 context 被取消的信号:当这个 channel 被关闭时,说明 context 被取消了。注意,这是一个只读的channel。 我们又知道,读一个关闭的 channel 会读出相应类型的零值。并且源码里没有地方会向这个 channel 里面塞入值。换句话说,这是一个 receive-only 的 channel。因此在子协程里读这个 channel,除非被关闭,否则读不出来任何东西。也正是利用了这一点,子协程从 channel 里读出了值(零值)后,就可以做一些收尾工作,尽快退出。
Err() 返回一个错误,表示 channel 被关闭的原因。例如是被取消,还是超时。
Deadline() 返回 context 的截止时间,通过此时间,函数就可以决定是否进行接下来的操作,如果时间太短,就可以不往下做了,否则浪费系统资源。当然,也可以用这个 deadline 来设置一个 I/O 操作的超时时间。
Value() 获取之前设置的 key 对应的 value。
canceler接口
type canceler interface {
cancel(removeFromParent bool, err error)
Done() <-chan struct{}
}
实现了上面定义的两个方法的 Context,就表明该 Context 是可取消的。源码中有两个类型实现了 canceler 接口:*cancelCtx 和 *timerCtx。注意是加了 * 号的,是这两个结构体的指针实现了 canceler 接口。
Context 接口设计成这个样子的原因:
- “取消”操作应该是建议性,而非强制性
caller 不应该去关心、干涉 callee 的情况,决定如何以及何时 return 是 callee 的责任。caller 只需发送“取消”信息,callee 根据收到的信息来做进一步的决策,因此接口并没有定义 cancel 方法。
- “取消”操作应该可传递
“取消”某个函数时,和它相关联的其他函数也应该“取消”。因此,Done() 方法返回一个只读的 channel,所有相关函数监听此 channel。一旦 channel 关闭,通过 channel 的“广播机制”,所有监听者都能收到。
emptyCtx结构体
emptyCtx是实现了context接口的结构体,不过其每个方法都返回零值。包内TODO和background就是返回emptyCtx的的对象。
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
}
cancelCtx结构体
cancelCtx结构体嵌套了context接口(也就是cancelCtx变量实现了context接口),同时他有Done()和cancel()两个成员方法,说明其实现了canceler接口。
c.done 是“懒汉式”创建,只有调用了 Done() 方法的时候才会被创建。再次说明,函数返回的是一个只读的 channel,而且没有地方向这个 channel 里面写数据。所以,直接调用读这个 channel,协程会被 block 住。一般通过搭配 select 来使用。一旦关闭,就会立即读出零值。
cancel() 方法的功能就是关闭 channel:c.done;递归地取消它的所有子节点;从父节点从删除自己。达到的效果是通过关闭 channel,将取消信号传递给了它的所有子节点。goroutine 接收到取消信号的方式就是 select 语句中的读 c.done 被选中。
type cancelCtx struct {
Context
// 保护之后的字段
mu sync.Mutex
done chan struct{}
children map[canceler]struct{}
err error
}
通过WithCancel函数可以返回一个新的context和cancelFun,当cancelFun被调用或者父节点done channel被关闭,此context的done channel也会被关闭。
timerCtx结构体
timerCtx 基于 cancelCtx,只是多了一个 time.Timer 和一个 deadline。Timer 会在 deadline 到来时,自动取消 context。
type timerCtx struct {
cancelCtx
timer *time.Timer // Under cancelCtx.mu.
deadline time.Time
}
WithDeadline()和WithTimeout()方法可以可以得到一个timerCtx的结构体对象。
valueCtx结构体
type valueCtx struct {
Context
key, val interface{}
}
通过valueCtx函数创建一个valueCtx对象。
func WithValue(parent Context, key, val interface{}) Context {
if key == nil {
panic("nil key")
}
if !reflect.TypeOf(key).Comparable() {
panic("key is not comparable")
}
return &valueCtx{parent, key, val}
}
value取值的过程,实际上是一个递归查找的过程:
它会顺着链路一直往上找,比较当前节点的 key 是否是要找的 key,如果是,则直接返回 value。否则,一直顺着 context 往前,最终找到根节点(一般是 emptyCtx),直接返回一个 nil。所以用 Value 方法的时候要判断结果是否为 nil。
因为查找方向是往上走的,所以,父节点没法获取子节点存储的值,子节点却可以获取父节点的值。
WithValue 创建 context 节点的过程实际上就是创建链表节点的过程。两个节点的 key 值是可以相等的,但它们是两个不同的 context 节点。查找的时候,会向上查找到最后一个挂载的 context 节点,也就是离得比较近的一个父节点 context。所以,整体上而言,用 WithValue 构造的其实是一个低效率的链表。