协程如何退出?

一个协程启动后,大部分情况需要等待里面的代码执行完毕,然后协程会自行退出。但是如果有一种情景,需要让协程提前退出怎么办呢

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 多协程安全,可以在多个协程中放心使用。