Go Context包

前言

        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

1.context接口

        context包的核心接口,其定义如下

// 上下文携带截止日期、取消信号以及跨 API 边界的其他值。
// Context 的方法可以被多个 goroutine 同时调用。
type Context interface {
	Deadline() (deadline time.Time, ok bool)
	Done() <-chan struct{}
	Err() error
	Value(key interface{}) interface{}
}

        Context接口内定义了四个方法,分别如下。

  • Deadline():需要返回当前Context被取消的时间,也就是截止时间。
  • Done():需要返回一个channel,该channel会在工作完成或者Context被取消时关闭,多次调用Done方法返回的是同一个channel。
  • Err():用于返回当前Context结束的原因,仅在Done方法返回的channel被关闭时才返回非空值,这里包含两种错误,如果当前Context被取消,则返回Canceled错误;如果当前Context超时,则返回DeadlineExceeded错误。
  • Value():用来取得当前Context上绑定的值,是一个键值对,所以参数是一个key值,多次调用该方法而参数相同的话,返回的结果也相同。

        虽然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都会被取消。

扫描二维码关注公众号,回复: 14723880 查看本文章

        下面来看一下这两个方法在源码中的实现:

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。 

2.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,并传递给子节点。下面介绍一下各个函数的作用。

  • WithCancel:parent Context根据参数创建一个新的children Context,同时还返回一个取消该children Context的函数CancelFunc。其主要作用是在parent和children之间同步取消或结束信号,确定parent被取消时,其children也会收到信号而被取消。其实现的原理是所有的children都被保存在一个map中,如果是Context执行了Done方法会返回done channel,此时是正常结束所以返回以后就完结了;而如果是通过Err方法结束,则会遍历Context的所有children并关闭其channel。
  • WithDeadline:与WithCancel类似,指定一个截止时间参数,到了截止时间会自动取消该Context。当截止时间发生后,子context将退出。因此子context的退出有3种时机,一种是父context退出;一种是超时退出;一种是主动调用cancel函数退出。
  • WithTimeout:与WithDeadline类似,指定超时时间。
  • WithValue:该函数的作用是生成一个绑定了一个键值对数据的Context,这个绑定的数据可以通过该Context的Value方法访问。该方法可以完成追踪功能,需要通过Context传递数据时可以使用该方法。

        Context对象的生存周期一般仅为一个请求的处理周期,即针对一个请求创建一个Context变量(它是上下文树结构的根)。在请求处理结束后,撤销此变量,释放资源。

        每次创建一个协程时,可以将原有的上下文传递给这个子协程,或者新创建一个子上下文传递给这个协程。上下文能灵活地存储不同类型、不同数目的值,并且使多个协程安全地读写其中的值。当通过父Context对象创建子上下文对象时,即可获得子上下文的一个取消函数,这样父上下文对象的创建环境就获得了对子上下文的撤销权。

        在协程中,childCtx是preCtx的子context,其设置的超时时间为300ms。但是preCtx的超时时间为100 ms,因此父context退出后,子context会立即退出,实际的等待时间只有100ms。

三、context应用

        在平时协程控制当中,我们常见应用采用通道、上下文以及sync包,通过这三者,完全可以达到完美控制协程运行的目的。

  • 使用sync.WaitGroup,它用于线程总同步,会等待一组线程集合完成,才会继续向下执行,这对监控所有子协程全部完成的情况特别有用,但要控制某个协程就无能为力了。
  • 使用通道来传递消息,一个协程发送通道信号,另一个协程通过select得到通道信息,这种方式可以满足协程之间的通信,控制协程运行。但如果协程数量达到一定程度,就很难把控了。或者这两个协程还和其他协程也有类似通信,例如A与B,B与C,如果A发信号B退出了,C有可能等不到B的通道信号而被遗忘。
  • 使用上下文来传递消息,上下文是层层传递机制,根节点完全控制了子节点,根节点(父节点)可以根据需要选择自动还是手动结束子节点。而每层节点所在的协程就可以根据信息来决定下一步的操作。

        这里主要介绍使用上下文控制协程的运行,两个协程都可以收到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
}

        执行结果如下(打印结果不一定是这样顺序输出):

猜你喜欢

转载自blog.csdn.net/qq_34272964/article/details/127099996