GoLang 使用 goroutine 停止的几种办法

1,362 阅读2分钟

[toc]

前言

我们有很多情况下需要主动关闭goroutine,如需要实现一个系统自动熔断的功能就需要主动关闭goroutine

为什么要中断GoRoutine?

场景:

俩个相互依赖的的操作,“依赖”是指如果其中一个失败,那么另一个就没有意义,而不是第二个操作依赖第一个操作的结果(那种情况下,两个操作不能并行)。在这种情况下,如果我们很早就知道其中一个操作失败,那么我们就会希望能取消所有相关的操作。

goroutine介绍

goroutine是Go语言实现并发编程的利器,是 Go语言中的轻量级线程实现,由 Go 运行时(runtime)管理,简单的一个指令go function就能启动一个goroutine;Go 程序会智能地将 goroutine 中的任务合理地分配给每个 CPU。

但是,Go语言并没有提供终止goroutine的接口,也就是说,我们不能从外部去停止一个goroutine,只能由goroutine内部退出(main函数终止除外);

几种停止的办法

1. 使用 for-range

for-range 从 channel 上接收值,直到 channel 关闭,该结构在Go并发编程中很常用,这对于从单一通道上获取数据去执行某些任务是十分方便的

package main

import (
	"fmt"
	"sync"
)

//源码&面试>>https://javapub.blog.csdn.net/category_11938137.html

var wg sync.WaitGroup //等待组,用来阻塞程序

func worker(ch chan int) {
	defer wg.Done() //等待组 -1
	for v := range ch {
		fmt.Println(v)
	}
}

func main() {

	ch := make(chan int)
	wg.Add(1) //等待组 +1
	go worker(ch)

	for i := 0; i < 5; i++ {
		ch <- i
	}

	close(ch) //必须要加close,因为在打印完0、1、2、3、4后会发生阻塞,直到chan关闭。
	wg.Wait()
}

在这里插入图片描述

去掉close的情况

在这里插入图片描述


2. 使用 for-select (向退出通道发出退出信号)

当channel比较多时,for-range结构借不是很方便了;

Go语言提供了另外一种和channel相关的语法: select;

select能够让goroutine在多个通信操作上等待(可以理解为监听多个channel);

由于这个特性,for-select结构在Go并发编程中使用的频率很高;

我在使用Go的开发中,这是我用的最多的一种组合形式:

这里用 quit通道接收退出信号。


package main

import (
	"fmt"
	"sync"
	"time"
)

var wg sync.WaitGroup

func worker(in, quit <-chan int) {
	defer wg.Done()
	for {
		select {
		case <-quit:
			fmt.Println("收到退出信号")
			return //必须return,否则goroutine不会结束
		case v := <-in:
			fmt.Println(v)
		}
	}
}

func main() {
	quit := make(chan int) //退出通道
	in := make(chan int)

	wg.Add(1)
	go worker(in, quit)

	for i := 0; i < 3; i++ {
		in <- i
		time.Sleep(1 * time.Second)
	}

	quit <- 1 //想通道写入退出信号
	wg.Wait()
}

在这里插入图片描述


3. 使用for-select(关闭退出通道)

当我们就需要向 quit 通道中发送100次数据,如果再用以上的代码就很麻烦,有一个很简单的方法,关闭 channel,这样所有监听 quit channel 的 goroutine 就都会收到关闭信号。

package main

//源码&面试>>https://javapub.blog.csdn.net/category_11938137.html

import (
	"fmt"
	"sync"
	"time"
)

var wg sync.WaitGroup

func worker(in, quit <-chan int) {
	defer wg.Done()
	for {
		select {
		case <-quit:
			fmt.Println("收到退出信号")
			return //必须return,否则goroutine不会结束
		case v := <-in:
			fmt.Println(v)
		}
	}
}

func main() {
	quit := make(chan int) //退出通道
	in := make(chan int)

	wg.Add(1)
	go worker(in, quit)

	for i := 0; i < 3; i++ {
		in <- i
		time.Sleep(1 * time.Second)
	}

	// quit <- 1 //想通道写入退出信号
	close(quit) // 直接关闭通道,程序退出
	wg.Wait()
}

在这里插入图片描述


4. 使用for-select(关闭多个channel)

如果select上监听了多个通道,需要所有的通道都关闭后才能结束goroutine,这里就利用select的一个特性,select不会在nil的通道上进行等待,因此将channel赋值为nil即可,此外,还需要利用channel的ok值。

package main

//源码&面试>>https://javapub.blog.csdn.net/category_11938137.html

import (
	"fmt"
	"sync"
	"time"
)

var wg sync.WaitGroup

func worker(in1, in2 <-chan int) {
	defer wg.Done()

	for {
		select {
		case v, ok := <-in1:
			if !ok {
				fmt.Println("收到退出信号1")
				in1 = nil
			}
			fmt.Println(v)
		case v, ok := <-in2:
			if !ok {
				fmt.Println("收到退出信号2")
				in2 = nil
			}
			fmt.Println(v)
		}
		if in1 == nil && in2 == nil {
			return
		}
	}
}

func main() {
	in1 := make(chan int)
	in2 := make(chan int)
	wg.Add(2)
	go worker(in1, in2)
	go worker(in1, in2)
	for i := 0; i < 3; i++ {
		in1 <- i
		time.Sleep(1 * time.Second)
		in2 <- i
	}
	close(in1)
	close(in2)
	wg.Wait()
}

在这里插入图片描述


5. 使用context包

context包是官方提供的一个用于控制多个goroutine写作的包;

使用context的cancel信号,可以终止goroutine的运行,context是可以向下传递的

package main

//源码&面试>>https://javapub.blog.csdn.net/category_11938137.html

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

func operation1(ctx context.Context) error {
	// 让我们假设这个操作会因为某种原因失败
	// 我们使用time.Sleep来模拟一个资源密集型操作
	time.Sleep(100 * time.Millisecond)
	return errors.New("failed")
}

func operation2(ctx context.Context) {
	// 我们使用在前面HTTP服务器例子里使用过的类似模式
	select {
	case <-time.After(500 * time.Millisecond):
		fmt.Println("done")
	case <-ctx.Done():
		fmt.Println("halted operation2")
	}
}

func main() {
	// 新建一个上下文
	ctx := context.Background()
	// 在初始上下文的基础上创建一个有取消功能的上下文
	ctx, cancel := context.WithCancel(ctx) //需要取消时,就调用cancel(),发出取消事件。
	// 在不同的goroutine中运行operation2
	go func() {
		operation2(ctx)
	}()

	err := operation1(ctx)
	fmt.Println(err)
	// 如果这个操作返回错误,取消所有使用相同上下文的操作
	if err != nil {
		cancel()
	}
}

// func main() {
// 	// 创建一个监听8000端口的服务器
// 	http.ListenAndServe(":8000", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 		ctx := r.Context()
// 		// 输出到 STDOUT 展示处理已经开始
// 		fmt.Fprint(os.Stdout, "processing request\n")
// 		// 通过select监听多个channel
// 		select {
// 		case <-time.After(2 * time.Second):
// 			// 如果两秒后接受到了一个消息后,意味请求已经处理完成
// 			// 我们写入"request processed"作为响应
// 			w.Write([]byte("request processed"))
// 		case <-ctx.Done():

// 			// 如果处理完成前取消了,在STDERR中记录请求被取消的消息
// 			fmt.Fprint(os.Stderr, "request cancelled\n")
// 		}
// 	}))
// }