0%

golang-context

什么是 context:
context 是golang用来设计的在多个Goroutine中传递上下文信息,包括:取消信号、超时时间、截止时间、k-v 等。
context.Context 类型的值可以协调多个 groutine 中的代码执行“取消”操作,并且可以存储键值对。最重要的是它是并发安全的。
随着 context 包的引入,标准库中很多接口因此加上了 context 参数,例如 database/sql 包。context 几乎成为了并发控制和超时控制的标准做法。

context 在设计上是一个多叉树,且不同的子context,在不同的groutine中是不会有影响的。而且context 实现中有的是基于channel,有的加有 lock所以是线程安全的。
一个context 的典型图

1
2
3
4
5
6
7
8
9
10
11
//如:net/http 中就是在 Listener后,产生一个context.Background,
// 随后 会有一个循环,一直接受 新的请求,并且启动一个goruntime去处理,在处理函数里会生成一个Cancel的context,
// 用来在处理完毕请求后,通知这个请求下所有 goruntime取消。
for {
.....
r := l.Accept()
go server(ctx) {
ctx,cancel := context.WithCancel(ctx)
}

}

在使用context的中,如果想监听 取消,或者超时的通知,需要在接受的goruntime里 监听 Context.Done()的方法,来订阅到超时或者取消的通知,或者是没什么用的

1
2
3
4
5
6
7
go fun(ctx) {
select {
case <-ctx.Done():
fmt.Print("context is cancel")
}

}

context的使用场景

  1. 想在多个goruntime 间传递数据,可以使用 ValueCtx,如 trace id
  2. 想在多个goruntime 控制时长,可以使用 timerCtx,需要在 多个 goruntime监听context.Done()方法的通知
    例如在业务的高峰期,某个下游服务的响应变慢,而当前系统的请求又没有超时控制,或者超时时间设置地过大,那么等待下游服务返回数据的协程就会越来越多。而我们知道,协程是要消耗系统资源的,后果就是协程数激增,内存占用飙涨,甚至导致服务不可用。更严重的会导致雪崩效应,整个服务对外表现为不可用.
  3. 想在多个goruntime 控制取消操作,可以使用 cancelCtx,需要在 多个 goruntime监听context.Done()方法的通知.
    当请求被取消,这有可能是使用者关闭了浏览器,请求方直接放弃了这次请求结果。这时,所有正在为这个请求工作的 goroutine 需要快速退出,因为它们的“工作成果”不再被需要了。在相关联的 goroutine 都退出后,系统就可以回收相关的资源.

context的结构

golang 官方默认提供了

  1. emptyCtx ,其中使用的Background,TODO 等方法都是返回的emptyCtx
  2. cancelCtx 作为有取消效果的 context
  3. timerCtx , 依赖于 cancelCtx,提供了时间的控制效果
  4. valueCtx ,可以传递值的 context

    context 接口

    接口规定了四个方法
  5. Deadline() (deadline time.Time, ok bool)
    返回代表此上下文完成工作的时间,对于没有截止日期时,截止日期返回 ok==false。
    timerCtx 会返回设置的过期时间,以及永远返回true
  6. Done() <-chan struct{}
    Done 当在 context 应该被取消的时候,返回一个close 的channel.
    WithCancel 当被调用 cancel 的时候,会被 close
    WithDeadline 当到期的时候会被 close
    WithTimeOut 当超时的时候,会close
    Done 用于在 select 语句中使用:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    func Stream(ctx context.Context, out chan<- Value) error {
    for {
    v, err := DoSomething(ctx)
    if err != nil {
    return err
    }
    select {
    case <-ctx.Done():
    return ctx.Err()
    case out <- v:
    }
    }
    }
  7. Err() error
    当context被关闭的时候,返回关闭的错误提示信息,否则返回nil
  8. Value(key interface{}) interface{}
    返回对应key 的value,如果没有匹配的返回nil.
    Value会在自身不匹配的时候向父context 匹配查询。直到找到,或者没有匹配的返回nil

    canceler 接口

    取消的接口定义,是可以直接主动触发取消的定义。
    实现是 *cancelCtx 和 *timerCtx
  9. cancel(removeFromParent bool, err error)
    removeFromParent 标识是否 从父context 中删除,只在 调用WithCancel返回的 cancel函数里设置为 true.
    err 是必传的,默认为 var Canceled = errors.New(“context canceled”)
  10. Done() <-chan struct{} 同 context的Done
    在cancelCtx的实现上,chan 是在调用Done方法的时候惰性创建的

emptyCtx

emptyCtx,作为调用 Background,TODO的时候返回的context ,如同它的名字 empty是一个空,无具体作用的 context。一般作为起始的parent context

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

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
}

func (e *emptyCtx) String() string {
switch e {
case background:
return "context.Background"
case todo:
return "context.TODO"
}
return "unknown empty Context"
}

var (
background = new(emptyCtx)
todo = new(emptyCtx)
)

valueCtx

作为可以在goruntime之间传递值的context,在获取value的时候,如果自身不匹配,会向父类查询。
直至找不到返回 nil

cancelCtx

控制类 cancelCtx, 当想可以控制goruntime的流程的时候,并且通知各个goruntime取消操作的时候使用。
自身具有 children map[canceler]struct{},用来存储子 cancelCtx。
也就是在 调用WithCancel函数的时候,会判断当前 parent context 是否是 cancelCtx,如果是会往parent context的 children添加新建的cancelCtx.
一边,在parent 到期或者取消的时候,parent 会遍历children 调用子context的cancel函数,通知子context取消。

timerCtx

WithDeadline和WithTimeOut 返回的都是timerCtx,其中 WithTimeOut 加上了超时的时间后,又调用了WithDeadline。
当接受到具有 timerCtx的时候,可以调用 Deadline来获取时间作对比,加以判断是否还有足够的时间执行。
而且WithDeadline 在创建的时候,会判断当前时间,如果时间已经大于 超时时间,会创建一个cancelCtx返回。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
if parent == nil {
panic("cannot create context from nil parent")
}
if cur, ok := parent.Deadline(); ok && cur.Before(d) {
// 当设置的时间已经过期的时候,直接返回一个 cancel
return WithCancel(parent)
}
c := &timerCtx{
cancelCtx: newCancelCtx(parent),
deadline: d,
}
propagateCancel(parent, c)
dur := time.Until(d)
if dur <= 0 {
c.cancel(true, DeadlineExceeded) // deadline has already passed
return c, func() { c.cancel(false, Canceled) }
}
c.mu.Lock()
defer c.mu.Unlock()
if c.err == nil {
// 启动一个定时器,到期执行cancel函数
c.timer = time.AfterFunc(dur, func() {
c.cancel(true, DeadlineExceeded)
})
}
return c, func() { c.cancel(true, Canceled) }
}