Context详解
同步和异步机制
同步和异步机制关注的是通信机制
同步:调用端需要等待被调用端返回信息才继续运行
异步:不用理会被调用端,一直执行
很显然,Go中的goroutine通信机制就是一种异步机制
goroutine之间的通信
因为goroutine一开始就不受控制了,所以我们必须要想到一种优雅的控制goroutine的方式。
全局变量
这是一种最简单最暴力的控制goroutine的方法,我们不断对一个全局变量判断,当满足某某条件时,让全局变量变为false或true,从而对goroutine进行控制
sync.WaitGroup
先介绍一下里面最重要的几个函数
var wg sync.WaitGroup
wg.Add(n) //增加n个进程
wg.Wait() //等待所有进程结束
wg.Done() //表示此进程结束,Wait需要等待的进程数减一
一个栗子
func main() {
t:=time.Now()
var wg sync.WaitGroup
wg.Add(2)
go func() {
fmt.Println("小黄好好看")
wg.Done()
}()
go func() {
fmt.Println("小程也好看")
time.Sleep(1*time.Second)
wg.Done()
}()
wg.Wait()
fmt.Println("done!")
t1:=time.Since(t)
fmt.Println("cost time:",t1)
} //通过sync.WaitGroup控制并发
我们知道,通过sync.WaitGroup控制并发是一种非常传统的方式了,想一想就能发现,当有很多goroutine以及goroutine嵌套的时候,sync包控制并发是非常繁琐的一件事情。
chan+select
这是一种安全且占用资源少的goroutine退出方式
栗子:
func main() {
stop:=make(chan bool)
go func() {
for {
select {
case <- stop:
fmt.Println("监控退出,停止...")
return
default:
fmt.Println("goroutine监控中")
time.Sleep(1*time.Second)
}
}
}()
time.Sleep(5*time.Second)
fmt.Println("使用chan通知停止监控")
stop<-true
}
因为chan是并发安全的 所以我们使用chan通知goroutine退出是一种比较优雅、安全的方式 但是还是能看出来:一旦goroutine数量过多 或者因为goroutine自身的链式关系 比如嵌套 衍生等等 那么chan还是很难成功处理这种场景的
Context
上面提到的嵌套goroutine和衍生等情况是存在的,比如一个网络API接口需要一个goroutine处理部分需求,这个goroutine可能又会开启其他的goroutine 所以我们需要一种可以跟踪goroutine的方案 才能够达到控制它们的目的 这就是Go官方指定的处理并发编程的工具:Context 中文翻译为上下文 。
Context在通知多个goroutine或者嵌套等待goroutine退出的时候是非常方便的,于是在go1.7版本之后,官方发行了context包,用于管理协程的退出
Context接口
type Context interface{
Deadline() (deadline time.Time,ok bool) //返回当前Context被取消的时间,也就是完成工作的截止时间和是否完成任务
Done() <-chan struct{} //Done方法需要返回一个只读的Channel 这个Channel会在当前工作完成或者上下文被取消之后关闭 多次调用Done方法会返回同一个Channel
Err() error //被取消返回Canceled错误 超时返回DeadlineExceeded错误
Value(key interface{}) interface{}
//给Context带上一个键值对
}
context控制一个goroutine:
//使用context重写上文
package main
import (
"context"
"fmt"
"time"
)
func main() {
ctx,cancel:=context.WithCancel(context.Background())
go func(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println("监控退出,停止...")
return
default:
fmt.Println("goroutine监控中")
time.Sleep(1*time.Second)
}
}
}(ctx)
time.Sleep(5*time.Second)
fmt.Println("使用context通知停止监控")
cancel()
}
context.Background() 返回一个空的context 这个空的context一般作用于整个Context树的根节点
Context的一些函数
context.Background() //返回一个空的Context 这个Context可以作为所有context的根节点
context.Todo() //目前没有具体的使用场景,当我们不知道该使用什么Context的时候,可以使用这个
上面两个方法实现了Context的所有接口 它们的本质是
emptyCtx结构体类型 不可取消 没有截止时间 也没有value
context.WithCancel(parent) //创建了一个可取消的子Context 创建一个可取消的子Context 然后我们把这个子Context当作参数传给goroutine,这样就可以使用这个子Context跟踪这个goroutine 然后再在goroutine中使用select <-ctx.Done()
判断是否要结束 如果收到了这个值 就返回结束goroutine,如果接收不到,那么就继续进行监控
cancel() //发送结束命令 这个函数就是WithCancel的第二个返回值
context控制多个goroutine:
func main() {
ctx,cannel:=context.WithCancel(context.Background())
go watch(ctx,"监控1")
go watch(ctx,"监控2")
go watch(ctx,"监控3")
time.Sleep(5*time.Second)
fmt.Println("使用context通知多个goroutine退出")
cannel()
}
func watch(ctx context.Context,name string) {
for {
select {
case <-ctx.Done():
fmt.Println(name,"监控退出,停止了...")
return
default:
fmt.Println(name,"goroutine监控中...")
time.Sleep(1*time.Second)
}
}
} //使用Context通知多个goroutine退出
每个goroutine传入的context都是context.Background()生成的同一个子context 当我们使用cancel函数通知退出 三个goroutine都i会收到消息 这就是Context的控制能力,它就像一个控制器一样,按下开关后,所有基于这个 Context和衍生的子Context只要是被当作参数传给了goroutine,都可以控制goroutine的退出,不得不说这是一种优雅的处理goroutine启动之后不可控的方法
Context的衍生继承
有了根Context如何衍生出更多的子Context呢?这就需要用到我们的With系列函数了
func WithCancel(parent Context) (ctx Context,cancel CancelFunc)
func WithDeadline(parent Context,deadline time.Time) (Context,CancelFunc)
func WitchTimeout(parent Context,timeout time.Time) (Context,CancelFunc)
func WithValue(parent Context,key,val interface{}) Context
这四个with函数接受的都是父亲Context 并且生成子Context 前三个返回值里面都有CanceFunc(取消函数),它就像一个控制器一样,一旦触发,无论子Context有多少个,叠加多少层都会一次性关闭。第二个With函数和第三个With函数都是设立一个过期时间,非常类似第四个函数是给Context绑定一个键值对
Context使用原则
-
不要把Context放在结构体里面,而是要当作参数传递
-
以Context作为参数的函数方法,应该把Context当作第一个参数
-
给一个函数方法传递Context的时候,如果不知道具体应该传什么,那么不要传nil,传context.Todo
-
context的value相关方法应该传递必须的函数,不要什么数据都用value尽心传递
-
Context是线程安全的,可以放心的在多个goroutine中传递