context是Go语言1.7版本加入官方库,官方常用于处理单个请求的多个goroutine与请求域的数据、截止时间、同步信号和传递请求值等相关的操作。学习context,有利于大家更好的使用goroutine,提升对并发编程的理解。
一、为什么需要context
在典型的http服务中,每一个http请求的request都会启动一个goroutine处理请求,此请求后续还可能操作数据库、缓存、日志等,此时由最早的goroutine启动后续的多个goroutine,这样就使用多个goroutine处理一个request请求,而context就是在几个不同goroutine直接同步数据、取消信号以及处理请求截至日期。
context最常规的做法就是从goroutine开始,一层层地把信息传递到最下层。如果没有context,就可能发生上层的已经因为报错而结束,但是下层的goroutine却还在继续。
如果有了context,当上层goroutine发生错误而结束时,可以很快地同步信息到其下层的goroutine,这样可以及时停止下层goroutine,避免无谓的系统消耗。
context包的核心接口,其定义如下
- // 上下文携带截止日期、取消信号以及跨 API 边界的其他值。
- // Context 的方法可以被多个 goroutine 同时调用。
- type Context interface {
- Deadline() (deadline time.Time, ok bool)
- Done() <-chan struct{}
- Err() error
- Value(key interface{}) interface{}
- }
Context接口内定义了四个方法,分别如下。
虽然Context是一个接口,但是标准包里面实现了其他的两个方法:Background方法和TODO方法,可通过这两个方法来使用Context。在介绍这两个方法之前,需要先介绍一下Context的实现。
Context在数据结构上是一种单向继承关系,最开始的Context起到类似于初始化的作用,里面有一些数据,下一层的Context会继承上一层的Context,新的Context可以有children,children就是在上一层的Context外面再套一层,新扩的一层可以存储与自己相关的数据。这种多层结构可以像启动goroutine一样扩展很多层。
理解了Context的分层模式,就可以方便地理解Background和TODO方法了,这两个方法用于返回私有化的变量background和todo,这两个变量就存储于最顶层的parent Context中,后续的Context都是衍生自这个parent,形成树状层次。当一个parent Context被取消时,继承自它的所有Context都会被取消。
下面来看一下这两个方法在源码中的实现:
- func Background() Context {
- return background
- }
-
- func TODO() Context {
- return todo
- }
background和todo两个私有变量是在context包初始化的时候就定义好的,Background和TODO这两个方法也没有什么差别,可以理解为二者互为别名,只是Background方法是每个Context的顶层默认值,用于main函数,以及初始化、测试等代码中,它作为根Context是不可以被取消的。而TODO方法则是在不确定的时候使用的,但现实中很少使用。
background和todo这两个私有变量其实是两个指针,指向emptyCtx结构体实例。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
- }
可以看到,本质上background和todo是不携带任何信息的Context,不可取消,没有截止时间;而衍生出来的Context都继承自这个根Context。
前面介绍了Context的分层模式,那么Context是如何实现退出和传递的呢?退出和传递靠context包提供的With系列函数实现的。下面来看一下With系列函数,一共有四个:
- func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {}
-
- func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {}
-
- func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {}
-
- func WithValue(parent Context, key, val interface{}) Context {}
这些函数都接收一个Context类型的参数parent,并返回一个Context类型的值,这样就层层创建出不同的节点。父节点创建Context,并传递给子节点。下面介绍一下各个函数的作用。
Context对象的生存周期一般仅为一个请求的处理周期,即针对一个请求创建一个Context变量(它是上下文树结构的根)。在请求处理结束后,撤销此变量,释放资源。
每次创建一个协程时,可以将原有的上下文传递给这个子协程,或者新创建一个子上下文传递给这个协程。上下文能灵活地存储不同类型、不同数目的值,并且使多个协程安全地读写其中的值。当通过父Context对象创建子上下文对象时,即可获得子上下文的一个取消函数,这样父上下文对象的创建环境就获得了对子上下文的撤销权。
在协程中,childCtx是preCtx的子context,其设置的超时时间为300ms。但是preCtx的超时时间为100 ms,因此父context退出后,子context会立即退出,实际的等待时间只有100ms。
在平时协程控制当中,我们常见应用采用通道、上下文以及sync包,通过这三者,完全可以达到完美控制协程运行的目的。
这里主要介绍使用上下文控制协程的运行,两个协程都可以收到cancel()发出的信号,B方法不结束协程可反复接收取消信息。
- package main
-
- import (
- "context"
- "fmt"
- "log"
- "os"
- "os/signal"
- "syscall"
- "time"
- )
-
- func A(ctx context.Context) string {
- ctx = context.WithValue(ctx, "funcA", "A")
-
- go B(ctx)
-
- // 监听上层的ctx
- select {
- case <-ctx.Done():
- fmt.Println("A Done")
- default:
- log.Println("func A: default")
- }
-
- return "AA"
- }
-
- func B(ctx context.Context) string {
- ctx = context.WithValue(ctx, "funcB", "B")
-
- go C(ctx)
-
- // 监听自己上层的ctx
- select {
- case <-ctx.Done():
- fmt.Println("B Done")
- return "B"
- default:
- log.Println("func B: default")
- }
-
- return "BB"
- }
-
- func C(ctx context.Context) string {
- ctx = context.WithValue(ctx, "funcCA", ctx.Value("funcA"))
- ctx = context.WithValue(ctx, "funcCB", ctx.Value("funcB"))
-
- // 监听自己上层的ctx
- select {
- case <-ctx.Done():
- fmt.Println("C Done")
- return "C"
- default:
- log.Println("func C: default")
- }
-
- return "CC"
- }
执行结果如下:
下面代码用上下文嵌套控制3个协程A,B,C。在主程序发出cancel信号后,每个协程都能接收根上下文的Done()信号而退出。
- package main
-
- import (
- "context"
- "fmt"
- "log"
- "os"
- "os/signal"
- "syscall"
- "time"
- )
-
- func A(ctx context.Context) string {
- ctx = context.WithValue(ctx, "funcA", "A")
-
- go B(ctx)
-
- // 监听上层的ctx
- select {
- case <-ctx.Done():
- fmt.Println("A Done")
- default:
- log.Println("func A: default")
- }
-
- return "AA"
- }
-
- func B(ctx context.Context) string {
- ctx = context.WithValue(ctx, "funcB", "B")
-
- go C(ctx)
-
- // 监听自己上层的ctx
- select {
- case <-ctx.Done():
- fmt.Println("B Done")
- return "B"
- default:
- log.Println("func B: default")
- }
-
- return "BB"
- }
-
- func C(ctx context.Context) string {
- ctx = context.WithValue(ctx, "funcCA", ctx.Value("funcA"))
- ctx = context.WithValue(ctx, "funcCB", ctx.Value("funcB"))
-
- // 监听自己上层的ctx
- select {
- case <-ctx.Done():
- fmt.Println("C Done")
- return "C"
- default:
- log.Println("func C: default")
- }
-
- return "CC"
- }
-
- func main() {
- // 新建一个ctx
- timeout := time.Second * 5
- ctx, _ := context.WithTimeout(context.Background(), timeout)
-
- log.Println("funcA 执行完成,返回:", A(ctx))
-
- select {
- case <-ctx.Done():
- log.Println("context Done")
- break
- }
-
- // 监听系统退出信号
- quit := make(chan os.Signal, 1)
- signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
- <-quit
- }
执行结果如下(打印结果不一定是这样顺序输出):
