我们在使用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 string, 6)
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() 方法来手动关闭