golang context包到底是干什么的?

1. 上下文context

上下文context.Context在golang中用于设置截止日期、同步信号、传递请求相关值的结构体。

上下文概念和goroutine有着较为密切的关系,是go中独有的设计。

 

在goroutine构成的树形结构中对信号进行同步以减少计算资源的浪费是context最大的作用。

go服务中每一个请求都是通过单独的goroutine处理的,http/rpc请求的处理器会启动新的goroutine访问数据库和其他服务。

我们可能会创建多个goroutine来处理一次请求,而context.Context的作用就是在不同的goroutine之间同步请求特定数据、取消信号以及处理请求的截止日期。

每一个context.Context都会从最顶层的goroutine一层一层传递给最下层。context.Context可以在上层goroutine执行出现错误时,将信号及时同步给下层。

2. 使用context同步信号

func main() {

	// 带1s超时的context
	// 指明了我们只有一秒的时间处理handle这个协程
	ctx, cancelFunc := context.WithTimeout(context.Background(), 1*time.Second)
	defer cancelFunc()

	go handle(ctx, 500*time.Millisecond)

	// main协程监听超时
	select {
	case <-ctx.Done():
		fmt.Println("main", ctx.Err())
	}
}

func handle(ctx context.Context, duration time.Duration) {

	// handle协程监听超时或任务完成--duration时间处理任务
	select {
	case <-ctx.Done():
		fmt.Println("handle", ctx.Err())
	case <-time.After(duration):
		fmt.Println("process request with ", duration)
	}
}

 

在上述的例子中,我们带超时的context有1s的超时时间,而我们需要处理的协程需要耗费500ms,所以协程不会被context打断,如果我们将handle协程的处理时间增加到2000ms,就会发现主协程的context中断了handle协程的执行:

handle context deadline exceeded
main context deadline exceeded

 

这个例子就是简单的利用context进行协程同步或者取消下游协程,多个goroutine同时订阅ctx.Done()管道中的消息,一旦收到取消信号就立即停止当前正在执行的工作。

 

3. 默认上下文

context包中最常用的方法还是context.Background和context.TODO,这两个方法都会返回预先初始化好的私有变量background和todo,他们会在一个go程序中被复用。

从源代码来看,context.Background和context.TODO只是互为别名,没有太大差别,只是在使用和语义上稍有不同:

  • context.Background是上下文的默认值,所有其他上下文都应该从他衍生而来
  • context.TODO应该仅在不确定该使用哪种上下文时使用

在多数情况下,如果当前函数没有上下文作为入参,我们都会使用context.Background作为起始的上下文传递

 

4. 取消信号

context.WithCancel函数能够从context.Context中衍生出一个新的子上下文并用于取消该上下文的函数。一旦我们执行返回的取消函数,当前上下文以及它的子上下文都会被取消,所有的goroutine都会收到这一取消信号。

我们直接从context.WithCancel函数的实现看看它做了什么:

  • context.newCancelCtx将传入的上下文包装成私有结构体context.cancelCtx
  • context.propagateCancel会构建父子上下文之间的关联,当父上下文被取消时,子上下文也会被取消
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
	c := newCancelCtx(parent)
	propagateCancel(parent, &c)
	return &c, func() { c.cancel(true, Canceled) }
}

 

func propagateCancel(parent Context, child canceler) {
	done := parent.Done()
	if done == nil {
		return // 父上下文不会触发取消信号
	}
	select {
	case <-done:
		child.cancel(false, parent.Err()) // 父上下文已经被取消
		return
	default:
	}

	if p, ok := parentCancelCtx(parent); ok {
		p.mu.Lock()
		if p.err != nil {
			child.cancel(false, p.err)
		} else {
			p.children[child] = struct{}{}
		}
		p.mu.Unlock()
	} else {
		go func() {
			select {
			case <-parent.Done():
				child.cancel(false, parent.Err())
			case <-child.Done():
			}
		}()
	}
}

  

上述函数包含了与父上下文相关的三种不同情况:

  • 当parent.Done() == nil,也就是parent不会触发取消事件时,当前函数会直接返回
  • 当child的继承链包含可以取消的上下文时,会判断parent是否已经触发了取消信号。如果已经被取消,child会立即被取消;如果被有被取消,child会加入到parent的children列表中,等待parent的取消信号
  • 当parent上下文是开发者自定义的类型,实现了Context接口并在Done()方法中返回了非空的管道时,运行一个新的goroutine同时监听parent.Done()和child.Done()两个chan,在parent.Done()关闭时调用child.cancel取消上下文

context.propagateCancel的作用是在parent和child之间同步取消和结束的信号,保证在parent被取消时,child也会收到对应的信号,不会出现状态不一致的情况

 

5. 传值方法

如何使用上下文传值,context包的context.WithValue能从父上下文中创建一个子上下文,传值的子上下文使用context.valueCtx类型

func WithValue(parent Context, key, val interface{}) Context {
	if key == nil {
		panic("nil key")
	}
	if !reflectlite.TypeOf(key).Comparable() {
		panic("key is not comparable")
	}
	return &valueCtx{parent, key, val}
}

 

context.valueCtx结构体会将除了value之外的Err、Deadline等方法代理到父上下文中,它只会响应context.valueCtx.Value方法,该方法的实现:

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

func (c *valueCtx) Value(key interface{}) interface{} {
	if c.key == key {
		return c.val
	}
	return c.Context.Value(key)
}

 

如果context.valueCtx中存储的键值对与context.valueCtx.Value方法中传入的参数不匹配,就会从父上下文中查找该键对应的值到某个父上下文中返回nil或者查找到对应的值

posted @ 2022-03-21 17:07  aganippe  阅读(625)  评论(0编辑  收藏  举报