背景
(欢迎关注“云原生手记”微信公众号)
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方法的功能有个了解就行。