协程如何退出?
一个协程启动后,大部分情况需要等待里面的代码执行完毕,然后协程会自行退出。但是如果有一种情景,需要让协程提前退出怎么办呢
func main() {
testContext1()
//testContext2()
//testContext3()
}
func testContext1() {
var wg sync.WaitGroup
wg.Add(1)
stopCh := make(chan bool) //用来停止监控狗
go func() {
defer wg.Done()
watchDog1(stopCh,"【监控狗1】")
}()
time.Sleep(5 * time.Second) //先让监控狗监控5秒
stopCh <- true //发停止指令
wg.Wait()
}
func watchDog1(stopCh chan bool,name string){
//开启for select循环,一直后台监控
for{
select {
case <-stopCh:
fmt.Println(name,"停止指令已收到,马上停止")
return
default:
fmt.Println(name,"正在监控……")
}
time.Sleep(1*time.Second)
}
}
同时取消很多个协程呢?
如果是定时取消协程又该怎么办?这时候 select+channel 的局限性就凸现出来了,必须有一种可以跟踪协程的方案,只有跟踪到每个协程,才能更好地控制它们,这就要用到 Go 语言标准库提供的 Context
func main() {
testContext2()
}
func testContext2() {
var wg sync.WaitGroup
wg.Add(2)
ctx,stop:=context.WithCancel(context.Background())
go func() {
defer wg.Done()
watchDog2(ctx,"【监控狗2】")
}()
go func() {
defer wg.Done()
watchDog2(ctx,"【监控狗3】")
}()
time.Sleep(5 * time.Second) //先让监控狗监控5秒
stop() //发停止指令
wg.Wait()
}
func watchDog2(ctx context.Context,name string) {
//开启for select循环,一直后台监控
for {
select {
case <-ctx.Done():
fmt.Println(name,"停止指令已收到,马上停止")
return
default:
fmt.Println(name,"正在监控……")
}
time.Sleep(1 * time.Second)
}
}
相比 select+channel 的方案,Context 方案主要有 4 个改动点:
1.watchDog 的 stopCh 参数换成了 ctx,类型为 context.Context。
2.原来的 case <-stopCh 改为 case <-ctx.Done(),用于判断是否停止。
3.使用 context.WithCancel(context.Background()) 函数生成一个可以取消的 Context,用于发送停止指令。这里的 context.Background() 用于生成一个空 Context,一般作为整个 Context 树的根节点。
4.原来的 stopCh <- true 停止指令,改为 context.WithCancel 函数返回的取消函数 stop()。
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}
1.Deadline 方法可以获取设置的截止时间,第一个返回值 deadline 是截止时间,到了这个时间点,Context 会自动发起取消请求,第二个返回值 ok 代表是否设置了截止时间。
2.Done 方法返回一个只读的 channel,类型为 struct{}。在协程中,如果该方法返回的 chan 可以读取,则意味着 Context 已经发起了取消信号。通过 Done 方法收到这个信号后,就可以做清理操作,然后退出协程,释放资源。
3.Err 方法返回取消的错误原因,即因为什么原因 Context 被取消。
4.Value 方法获取该 Context 上绑定的值,是一个键值对,所以要通过一个 key 才可以获取对应的值。
Context 接口的四个方法中最常用的就是 Done 方法
它返回一个只读的 channel,用于接收取消信号。当 Context 取消的时候,会关闭这个只读 channel,也就等于发出了取消信号
1.WithCancel(parent Context):生成一个可取消的 Context。
2.WithDeadline(parent Context, d time.Time):生成一个可定时取消的 Context,参数 d 为定时取消的具体时间。
3.WithTimeout(parent Context, timeout time.Duration):生成一个可超时取消的 Context,参数 timeout 用于设置多久后取消
4.WithValue(parent Context, key, val interface{}):生成一个可携带 key-value 键值对的 Context。
以上四个生成 Context 的函数中,前三个都属于可取消的 Context,它们是一类函数,最后一个是值 Context,用于存储一个 key-value 键值对。
使用 Context 取消多个协程,传值
func testContext3() {
var wg sync.WaitGroup
wg.Add(2)
ctx,stop:=context.WithCancel(context.Background())
valCtx1:=context.WithValue(ctx,"userId",2)
go func() {
defer wg.Done()
getUserID(valCtx1)
}()
valCtx2:=context.WithValue(ctx,"user","JOJO")
go func() {
defer wg.Done()
getUser(valCtx2)
}()
time.Sleep(5 * time.Second) //先让监控狗监控5秒
stop() //发停止指令
wg.Wait()
}
func getUser(ctx context.Context){
for {
select {
case <-ctx.Done():
user:=ctx.Value("user")
fmt.Println("【获取用户】","协程退出",user)
return
default:
user:=ctx.Value("user")
fmt.Println("【获取用户】","用户为:",user)
time.Sleep(1 * time.Second)
}
}
}
func getUserID(ctx context.Context){
for {
select {
case <-ctx.Done():
userId:=ctx.Value("userId")
fmt.Println("【获取ID】","协程退出",userId)
return
default:
userId:=ctx.Value("userId")
fmt.Println("【获取ID】","用户ID为:",userId)
time.Sleep(1 * time.Second)
}
}
}
要更好地使用 Context,有一些使用原则需要尽可能地遵守。
1.Context 不要放在结构体中,要以参数的方式传递。
2.Context 作为函数的参数时,要放在第一位,也就是第一个参数。
3.要使用 context.Background 函数生成根节点的 Context,也就是最顶层的 Context。
4.Context 传值要传递必须的值,而且要尽可能地少,不要什么都传。
5.Context 多协程安全,可以在多个协程中放心使用。