如何实现Go goroutine安全退出

885 阅读5分钟

我们在使用goroutine进行并发处理时,经常会遇到主协程main函数执行完毕后程序退出,而goroutine还未完成处理就被迫终止的问题,例子如下:

package main

import (
    "fmt"
)

func test(){
    for i:=0;i<10;i++{
        fmt.Println(i)
    }

}

func main(){
    go test()
    //time.Sleep(1*time.Second)
    fmt.Println("aaaaaaaa")
}
/**
aaaaaaaa
0
1
*/

对于上面的结果你可能感觉到很困惑,为什么aaaaaaaa在上面,为什么test函数里的内容没有输出,下面来说明一下goroutine的两个特点:

  • 当一个新的goroutine开始调用时,他会立即返回结果,而里面的内容在稍后执行,也就是在上面的例子中main函数里,test()调用里面的逻辑处理并不执行,而是直接返回,稍后他在自己去处理耗时如任务,这就是为什么aaa在前面
  • main函数的执行其实也是一个goroutine,这个作为支撑的goroutine如果运行结束,他内部衍生的goroutine也都会终止,这也是上面只是打印了0 1而不是0-9,想要都输出出来 可以给他足够的时间去处理,比如将上面注释打开time.Sleep(1*time.Second) 等待1s会出现你想要的结果

因此,我们如果想要正确的执行我们的程序代码,把握好goroutine的退出时间很有必要。以下是我总结的控制goroutine安全退出的几种方法:

goroutine安全退出方法

一、通过time.Sleep()方法

该方法是最简单的方法,只需要在主协程main函数的最后设置一段睡眠时间,例如5s,就可以使子协程执行完毕,顺利退出

func main() {
	for i := 0; i < 5; i++ {
		go func(i int) {   //通过匿名函数创建子协程
			tmp := rand.Intn(10)

			time.Sleep(time.Duration(tmp) * time.Second)
			fmt.Println("I want to sleep ", tmp, " seconds!")
			// c <- i
		}(i)
	}
	time.Sleep(5 * time.Second)   //设置5秒睡眠时间
}	

二、通过time包,实现定时控制退出

通过设置定时器的方法,使函数在定时器在到时之后再执行其他操作

2.1 设置time.Ticker定时器

Ticker是每隔一段时间就会触发一次

   func main() {
	ticker := time.NewTicker(5 * time.Second)   //设置一个5秒的定时器

	c := make(chan int, 5)

	for i := 0; i < 5; i++ {
		go func(i int) {
			fmt.Println("I want to sleep", i, "seconds!")
			c <- i
		}(i)
	}
        //for循环对channel的内容以及ticker进行监控
	for {
		select {
		case i := <-c:
			fmt.Printf("The %d goroutine is done.\n", i)
		case <-ticker.C:     //在定时器到时后,程序退出
			fmt.Println("Time to go out!")
			os.Exit(1)
		}
	}
}
2.2 设置time.Timer定时器

Timer只会触发一次

   func main() {
	timer := time.NewTimer(5 * time.Second)   //设置一个5秒的定时器

	c := make(chan int, 5)

	for i := 0; i < 5; i++ {
		go func(i int) {
			fmt.Println("I want to sleep", i, "seconds!")
			c <- i
		}(i)
	}
        //for循环对channel的内容以及ticker进行监控
	for {
		select {
		case i := <-c:
			fmt.Printf("The %d goroutine is done.\n", i)
		case <-timer.C:     //在定时器到时后,程序退出
			fmt.Println("Time to go out!")
			os.Exit(1)
		}
	}
}
2.3 通过time.After()实现超时控制
func doBadthing(done chan bool) {
	time.Sleep(time.Second)
	done <- true
}

func timeout(f func(chan bool)) error {
	done := make(chan bool)
	go f(done)
	select {
	case <-done:
		fmt.Println("done")
		return nil
	case <-time.After(time.Millisecond):
		return fmt.Errorf("timeout")
	}
}

// timeout(doBadthing)

利用 time.After 启动了一个异步的定时器,返回一个 channel,当超过指定的时间后,该 channel 将会接受到信号。

三、通过sync.WaitGroup{}等待组

通过设置sync.WaitGroup等待组,是控制goroutine很常用的一种方法,而且sync.WaitGroup的使用也非常简单,只需要Add()、Done()、Wait()三个函数就足够

func main() {
     wg := sync.WaitGroup{}   //初始化等待组
       
     wg.Add(5)    //设置协程的个数,因为有5个协程,所以设置为5
     for i := 0; i < 5; i++ {
           go func(i int) {
              fmt.Println("I want to sleep", i, "seconds!")         
              wg.Done()  //每当一个协程执行完成后,会通过Done()减一
           }(i)
      }        
      wg.Wait()    //只有当所有协程执行完成后才会往下执行,否则就会阻塞在此处

      fmt.Println("game over")
}
/*
I want to sleep 4 seconds!
I want to sleep 1 seconds!
I want to sleep 0 seconds!
I want to sleep 3 seconds!
I want to sleep 2 seconds!
game over
*/

四、通过channel优雅退出

通过channel退出goroutine是最主要的方式,goroutine虽然不能强制结束另外一个goroutine,但是可以通过channel发送信号通知另外的goroutine退出

func main() {
     ch := make(chan string6)
     done := make(chan struct{})    //声明一个channel,用于作为信号量处理 goroutine 的关闭
     go func() {
          for {
              select {
                 case ch <- "随便说点什么吧":
                 case <-done:      //当done这个channel中接收到信号,说明要关闭掉ch
                     close(ch)
                     return
              }
              time.Sleep(100 * time.Millisecond)
         }
     }()

    go func() {
          time.Sleep(3 * time.Second)   
          done <- struct{}{}    //休眠3秒后,发送关闭ch的信号到done中
    }()

    for i := range ch {
          fmt.Println("接收到的值: ", i)
    }

    fmt.Println("结束")
}
扩展

go语言中select是随机进行匹配的,在运行select时,会遍历所有(如果有机会的话)的 case 表达式,只要有一个信道有接收到数据,那么 select 就结束。select在执行过程中,必须命中其中的某一分支。如果在遍历完所有的 case 后,若没有命中任何一个case 表达式,就会进入default里的代码分支。但如果你没有写default 分支,select 就会阻塞,直到有某个 case 可以命中,而如果一直没有命中,select 就会抛出 deadlock 的错误

五、通过context通知goroutine退出

context在go语言中的地位举足轻重,context的本质还是一个channel数据,只是我们对其进行了一定的封装,通过ctx.Done()进行获取

func main() {
     ch := make(chan struct{})
     ctx, cancel := context.WithCancel(context.Background())
     //也可以通过context.WithTimeout设置结束时间
     //ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)

     go func(ctx context.Context) {
          for {
             select {
                 case <-ctx.Done():
                      fmt.Println("退出协程...")
                      ch <- struct{}{}
                      return
                 default:
                      fmt.Println("再说点啥吧...")
             }

             time.Sleep(500 * time.Millisecond)
          }
     }(ctx)

     go func() {
         time.Sleep(3 * time.Second)
         cancel()      //休眠3秒后,通过调用cancel()方法关闭
     }()

     <-ch
     fmt.Println("结束")
}

context中,我们可以通过ctx.Done()获取一个只读的channel,类型为结构体,可用于识别当前channel是否关闭,context 对于跨 goroutine 控制有自己的灵活之处,可以调用 context.WithTimeout() 来根据时间控制,也可以自己主动地调用 cancel() 方法来手动关闭