Golang并发编程-协程goroutine任务取消(Context)

文章目录


前言

在实际的业务种,我们可能会有这么一种场景:需要我们主动的通知某一个goroutine结束。比如我们开启一个后台goroutine一直做事情,比如监控,现在不需要了,就需要通知这个监控goroutine结束,不然它会一直跑,就泄漏了。

我们都知道一个goroutine启动后,我们是无法控制他的,大部分情况是等待它自己结束,那么如果这个goroutine是一个不会自己结束的后台goroutine呢?比如监控等,会一直运行的。

下面我们分别介绍 chan+select 方式 和 Context方式任务的取消的方法。


一、单个任务的取消

下面的代码,代表的场景是,一个监控的任务一直在执行中,通过一个无缓存信道,在接受到bool的值的时候,协程内部通过多路复用进入监控停止的逻辑,退出任务。

go 复制代码
package main

import (
	"fmt"
	"time"
)

func cancel_1(stop chan bool) {
	stop <- true
}

func monitor(name string, stop chan bool) {
	for {
		select {
		case <-stop:
			fmt.Printf("%v 监控退出,停止了...\n", name)
			return
		default:
			fmt.Printf("%v, goroutine监控中... \n", name)
			time.Sleep(2 * time.Second)
		}
	}
}

func main() {
	stop := make(chan bool)

	go monitor("1号", stop)

	time.Sleep(10 * time.Second)
	fmt.Println("可以了,通知监控停止")
	cancel_1(stop)
	//为了检测监控过是否停止,如果没有监控输出,就表示停止了
	time.Sleep(5 * time.Second)
}

代码执行结果如下:

bash 复制代码
[root@work day01]# go run context.go 
1号, goroutine监控中... 
1号, goroutine监控中... 
1号, goroutine监控中... 
1号, goroutine监控中... 
1号, goroutine监控中... 
可以了,通知监控停止
1号 监控退出,停止了...

现在,稍微改造下上面的代码,我们再增加几个goroutine。2号,3号 。。。

go 复制代码
func main() {
	stop := make(chan bool)

	go monitor("1号", stop)
	go monitor("2号", stop)
	go monitor("3号", stop)
	.....
	....
}

再次执行查看结果:

bash 复制代码
[root@work day01]# go run context.go 
3号, goroutine监控中... 
1号, goroutine监控中... 
2号, goroutine监控中... 
3号, goroutine监控中... 
1号, goroutine监控中... 
2号, goroutine监控中... 
2号, goroutine监控中... 
1号, goroutine监控中... 
3号, goroutine监控中... 
1号, goroutine监控中... 
3号, goroutine监控中... 
2号, goroutine监控中... 
1号, goroutine监控中... 
2号, goroutine监控中... 
3号, goroutine监控中... 
可以了,通知监控停止
1号 监控退出,停止了...
3号, goroutine监控中... 
2号, goroutine监控中... 
2号, goroutine监控中... 
3号, goroutine监控中... 
3号, goroutine监控中... 
2号, goroutine监控中... 

发现这种方式,只停了其中的一个任务。其他两个任务还在执行。

如何保证所有的任务都停止呢,我们看下一节

二、 所有任务取消

我们改造代码如下:

新增一个cancel_2的方法,再main函数调用cancel_2。

go 复制代码
package main

import (
	"fmt"
	"time"
)

func cancel_1(stop chan bool) {
	stop <- true
}

func cancel_2(stop chan bool) {
	close(stop)
}

func monitor(name string, stop chan bool) {
	for {
		select {
		case <-stop:
			fmt.Printf("%v 监控退出,停止了...\n", name)
			return
		default:
			fmt.Printf("%v, goroutine监控中... \n", name)
			time.Sleep(2 * time.Second)
		}
	}
}

func main() {
	stop := make(chan bool)

	go monitor("1号", stop)
	go monitor("2号", stop)
	go monitor("3号", stop)

	time.Sleep(10 * time.Second)
	fmt.Println("可以了,通知监控停止")
	cancel_2(stop)
	//为了检测监控过是否停止,如果没有监控输出,就表示停止了
	time.Sleep(5 * time.Second)

}

执行结果如下:

bash 复制代码
[root@work day01]# go run context.go 
3号, goroutine监控中... 
1号, goroutine监控中... 
2号, goroutine监控中... 
2号, goroutine监控中... 
1号, goroutine监控中... 
3号, goroutine监控中... 
1号, goroutine监控中... 
2号, goroutine监控中... 
3号, goroutine监控中... 
3号, goroutine监控中... 
1号, goroutine监控中... 
2号, goroutine监控中... 
1号, goroutine监控中... 
2号, goroutine监控中... 
3号, goroutine监控中... 
可以了,通知监控停止
3号 监控退出,停止了...
2号 监控退出,停止了...
1号 监控退出,停止了...

可以看到,三个goroutine都停止了,这里我们用到了关闭通道的广播机制,所有通道接收者都会收到关闭的消息。

上面的例子,说明当我们定义一个无缓冲通道时,如果要对所有的 goroutine 进行关闭,可以使用 close 关闭通道,然后在所有的 goroutine 里不断检查通道是否关闭(前提你得约定好,该通道你只会进行 close 而不会发送其他数据,否则发送一次数据就会关闭一个goroutine,这样会不符合咱们的预期,所以最好你对这个通道再做一层封装做个限制)来决定是否结束 goroutine。

三、Context的出现

前两节,chan+select的方式,是比较优雅的结束一个goroutine的方式,不过这种方式也有局限性,如果有很多goroutine都需要控制结束怎么办呢?如果这些goroutine又衍生了其他更多的goroutine怎么办呢?如果一层层的无穷尽的goroutine呢?如下图,当取消Handl(Req1)这个节点的任务,希望取消与他关联的子任务的时候。我们再通过上面方式是不是就需要修改很多呢。所以Context的真正用途出现了。

Context的定义

Context,也叫上下文,它的接口定义如下:

go 复制代码
type Context interface {
    Deadline() (deadline time.Time, ok bool)
    Done() <-chan struct{}
    Err() error
    Value(key interface{}) interface{}
}
  • Deadline:返回的第一个值是 截止时间,到了这个时间点,Context 会自动触发 Cancel 动作。返回的第二个值是
    一个布尔值,true 表示设置了截止时间,false 表示没有设置截止时间,如果没有设置截止时间,就要手动调用 cancel 函数取消Context。
  • Done:返回一个只读的通道(只有在被cancel后才会返回),类型为 struct{}。当这个通道可读时,意味着parent
    context已经发起了取消请求,根据这个信号,开发者就可以做一些清理动作,退出goroutine。
  • Err:返回 context 被 cancel 的原因。
  • Value:返回被绑定到 Context 的值,是一个键值对,所以要通过一个Key才可以获取对应的值,这个值一般是线程安全的。

Context使用

使用Context完成

go 复制代码
package main

import (
	"context"
	"fmt"
	"time"
)

func monitor(name string, ctx context.Context) {
	for {
		select {
		case <-ctx.Done():
			fmt.Printf("%v 监控退出,停止了...\n", name)
			return
		default:
			fmt.Printf("%v, goroutine监控中... \n", name)
			time.Sleep(2 * time.Second)
		}
	}
}

func main() {

	ctx, cancel := context.WithCancel(context.Background())

	go monitor("1号", ctx)
	go monitor("2号", ctx)
	go monitor("3号", ctx)

	time.Sleep(10 * time.Second)
	fmt.Println("可以了,通知监控停止")
	cancel()
	//为了检测监控过是否停止,如果没有监控输出,就表示停止了
	time.Sleep(5 * time.Second)
}

执行结果:

bash 复制代码
[root@work day01]# go run context2.go 
1号, goroutine监控中... 
3号, goroutine监控中... 
2号, goroutine监控中... 
1号, goroutine监控中... 
3号, goroutine监控中... 
2号, goroutine监控中... 
2号, goroutine监控中... 
3号, goroutine监控中... 
1号, goroutine监控中... 
1号, goroutine监控中... 
2号, goroutine监控中... 
3号, goroutine监控中... 
2号, goroutine监控中... 
1号, goroutine监控中... 
3号, goroutine监控中... 
可以了,通知监控停止
3号 监控退出,停止了...
1号 监控退出,停止了...
2号 监控退出,停止了...

总结

  • 任务的取消,一般可以采用chan + select方式,但是任务依赖比较复杂的情况推荐使用Context
  • 通常 Context 都是做为函数的第一个参数进行传递(规范性做法),并且变量名建议统一叫 ctx
  • Context 是线程安全的,可以放心地在多个 goroutine 中使用。
  • 当你把 Context 传递给多个 goroutine 使用时,只要执行一次 cancel 操作,所有的 goroutine 就可以收到
    取消的信号
  • 当一个 Context 被 cancel 时,继承自该 Context 的所有 子 Context 都会被 cancel。
相关推荐
学步_技术3 分钟前
Python编码系列—Python工厂方法模式:构建灵活对象的秘诀
开发语言·python·工厂方法模式
Deryck_德瑞克20 分钟前
Java集合笔记
java·开发语言·笔记
MengYiKeNan26 分钟前
C++二分函数lower_bound和upper_bound的用法
开发语言·c++·算法
会发paper的学渣31 分钟前
python 单例模式实现
开发语言·python·单例模式
Lingbug34 分钟前
.Net日志组件之NLog的使用和配置
后端·c#·.net·.netcore
学步_技术39 分钟前
Python编码系列—Python桥接模式:连接抽象与实现的桥梁
开发语言·python·桥接模式
计算机学姐40 分钟前
基于SpringBoot+Vue的篮球馆会员信息管理系统
java·vue.js·spring boot·后端·mysql·spring·mybatis
柴华松42 分钟前
GPU训练代码
开发语言·python
好兄弟给我起把狙1 小时前
[Golang] Select
开发语言·后端·golang
Echo_Lee01 小时前
C#与Python脚本使用共享内存通信
开发语言·python·c#