背景

(欢迎关注“云原生手记”微信公众号)
golang中并发编程的三种实现方式:chan管道、waitGroup和Context。本篇将重点介绍context的使用,告诉大家基本的使用方式,做到会用。

Context

概念介绍

在这里插入图片描述

context译为上下文,golang在1.6.2的时候还没有自己的context,在1.7的版本中就把golang.org/x/net/context包被加入到了官方的库中。golang 的 Context包,是专门用来处理多个goroutine之间与请求域的数据、取消信号、截止时间等相关操作。
使用场景:当请求来临时,你需要使用多个子协程去处理数据,但是此时业务报错,你需要去取消子协程并对请求做返回,直接返回不管子协程就有可能造成脏数据,而且子协程可能占用系统资源,所以你是需要关闭子协程的。而context就可以提供当子协程正在运行时,父协程可以关闭子协程这个功能。
context的呈现的形式像二叉树结构,有父子关系,父协程管理子协程。context的使用场景就是主协程管理多个子协程,这边的管理就是简单粗暴的关闭子协程。这种粗暴的关闭协程的方式分为了三种:context.WithCancel方法、context.WithTimeout方法和context.WithDeadline方法,包含主动关闭和被动关闭方式。下面我就这些方法的使用给个示例。

常用方法示例

context.WithCancel方法

下面是使用了context.WithCancel方法生成了当前协程的context ctx和cancel函数,取消函数CancelFunc,这是一个函数类型,它的定义非常简单如下:

type CancelFunc func()

,然后把ctx传递到当前协程管理的子协程中,然后通过cancel函数对子协程进行管理(主要的管理方式就是强制关闭子协程),如下程序:

func CancelTest() {
	ctx, cancel := context.WithCancel(context.Background())
	go func(ctx context.Context) {
		for {
			select {
			case <-ctx.Done():
				fmt.Println("协程退出,停止了...")
				return
			default:
				fmt.Println("协程运行中...")
				time.Sleep(2 * time.Second)
			}
		}
	}(ctx)
	time.Sleep( time.Second * 30)
	fmt.Println("两分钟时间到了,关闭子协程")
	cancel()
	time.Sleep( time.Second * 10)
	fmt.Println("演示结束")
}

上面的程序在子协程运行了30s后就会被主协程关闭。

context.WithTimeout方法

context.WithTimeout方法返回context ctx和cancel函数,这个WithTimeout方法的参数和WithCancel相比多了时间参数,就是可以设定一个时间,超过该时间就会主动触发cancel函数,触发原理是定时器Timer,当然你也可以主动使用cancel函数触发:

func TimeOutTest() {
	ctx, _ := context.WithTimeout(context.Background(), time.Minute * 1)
	go func(ctx context.Context) {
		for {
			select {
			case <-ctx.Done():
				fmt.Println("协程退出,停止了...")
				return
			default:
				fmt.Println("协程运行中...")
				time.Sleep(2 * time.Second)
			}
		}
	}(ctx)
	time.Sleep(time.Second * 70)
	fmt.Println("演示结束")
}

上面的函数,子协程会在运行1分钟后主动退出,然后接着10秒后主协程也会运行完退出。你也可以使用cancel函数在1分钟时间未到时主动调用使得子协程退出。

context.WithDeadline方法

context.WithDeadline和context.WithTimeout这两个方法的功能是相似的,区别是WithDeadline可以在指定任意时刻调用cancel函数,而WithTimeout只能基于调用时的时间然后加上一段时间调用cancel函数。

func DeadLineTest() {
	ctx, _ := context.WithDeadline(context.Background(), time.Now().Add(time.Minute * 1))
	go func(ctx context.Context) {
		for {
			select {
			case <-ctx.Done():
				fmt.Println("协程退出,停止了...")
				return
			default:
				fmt.Println("协程运行中...")
				time.Sleep(2 * time.Second)
			}
		}
	}(ctx)
	time.Sleep(time.Second * 70)
	fmt.Println("演示结束")
}

上面函数中context.WithDeadline需要传入的是个时间点,我写的时间点时time.Now().Add(time.Minute * 1),当前时间后的一分钟触发cancel函数(你可以选择任意时间点),所以子协程会在运行一分钟后结束运行,当然未到一分钟时,你可以主动调用withDeadline方法所返回的cancel函数对子协程提前管理。

context.WithValue方法

WithValue函数的返回只有上下文context,传入值多了一个key-value键值对,这个键值对可以在子协程中使用Context.Value方法访问到, 如下:

func WithValueTest() {
	ctx, cancel := context.WithCancel(context.Background())
	//附加值
	valueCtx := context.WithValue(ctx,"test","子协程1")
	go func(ctx context.Context) {
		for {
			select {
			case <-ctx.Done():
				//取出值
				fmt.Println(ctx.Value("test"), "监控退出,停止了...")
				return
			default:
				//取出值
				fmt.Println(ctx.Value("test"), "goroutine监控中...")
				time.Sleep(2 * time.Second)
			}
		}
	}(valueCtx)
	time.Sleep(10 * time.Second)
	fmt.Println("可以了,通知监控停止")
	cancel()
	//为了检测监控过是否停止,如果没有监控输出,就表示停止了
	time.Sleep(5 * time.Second)
}

将配置好的key-value的context传入子协程,然后子协程可以使用context.value()方法访问到值,这在各个父子关资的协程中传值比较方便。
####context 的使用规则
简单介绍下context 的使用规则,以免误用:

  • context 应该作为函数参数传递,而不是 struct 的一个 field
  • context 应该是函数的第一个参数
  • 不要传递 nil context,即使是不使用,如果不知道用啥,用 context.TODO 吧
  • 只使用 context 传递请求上下文,而不是为了传递可选参数
  • context 可能会被同一个函数在不同的 goroutine 中使用,他是并发安全的

context的缺陷

context虽然实现父协程对子协程的管理,但是这种管理方式是比较粗暴的,直接关闭,而且关闭时根本不知道子协程的执行结果。总之,对子协程的管理不够细致化,必要时需要在字协程退出时用defer做下退出处理,或者你可以使用waitGroup这种,对协程的执行结果有个明确的了解。

总结

本篇主要讲解了golang context在协程管理方面的使用,只是基础,大家只要记住当遇到父协程管理子协程这个场景时可以想到context即可。对于里面的4个基本方法:context.WithCancel方法、context.WithTimeout方法、context.WithDeadline方法和context.WithValue方法的功能有个了解就行。