GO语言中协程中的协程不会退出的 "陷阱"
版权声明:
                        
                    
                                本文为博主原创文章,转载请声明原文链接...谢谢。o_0。
                    更新时间:
                    
                2023-08-15 14:06:46
                温馨提示:
                    
            学无止境,技术类文章有它的时效性,请留意文章更新时间,如发现内容有误请留言指出,防止别人"踩坑",我会及时更新文章
                前言
熟悉Win下C++开发的都知道多线程,以及线程中的线程,当线程的上级线程退出后,里面的子线程都会强制退出。下面来到go语言中,go里面是用的协程,协程是比线程更小粒度的并发处理方式,并且资源开销很小。
协程测试
看下面示例,启动一个协程,并在协程里启动一个子协程,父协程3秒后退出,子协程一直执行没有退出条件,main函数等待10秒
package main
import (
    "fmt"
    "time"
)
func test() {
    go func() { //父协程
       defer func() {
          fmt.Println("exit 父协程")
       }()
       go func() { //子协程
          defer func() {
             fmt.Println("exit 子协程")
          }()
          
          // 子协程任务
          for {
             fmt.Println("running 子协程")
             time.Sleep(time.Second)
          }
       }()
       // 父协程任务
       i := 0
       for {
          fmt.Println("running 父协程")
          time.Sleep(time.Second)
          i++
          if i > 3 {
             break
          }
       }
    }()
    fmt.Println("exit test函数")
}
func main() {
    test()
    time.Sleep(time.Second * 10)
    fmt.Println("exit Main函数")
}运行结果

test函数直接退出,然后父协程和子协程交替运行,3秒后父协程退出,这时注意子协程还在执行,一直到main退出,子协程还没有打印退出日志。
可以看出go里面的协程是不会自动退出的,除非main函数退出,并且不会调用你预定的退出日志。
解决方案
Go语言设计本就是如此,需要你手动退出协程,并且它提供了context,channel等都可以优雅的让你退出协程,下面是context的退出示例
package main
import (
    "context"
    "fmt"
    "time"
)
func test() {
    go func() { //父协程
       ctx, cancel := context.WithCancel(context.Background())
       defer cancel()
       defer func() {
          fmt.Println("exit 父协程")
       }()
       go func(ctx context.Context) { //子协程
          defer func() {
             fmt.Println("exit 子协程")
          }()
          // 子协程任务
          for {
             select {
             case <-ctx.Done():
                fmt.Println("exit 子协程-收到父协程退出信号")
                return
             default:
                fmt.Println("running 子协程")
                time.Sleep(time.Second)
             }
          }
       }(ctx)
       // 父协程任务
       i := 0
       for {
          fmt.Println("running 父协程")
          time.Sleep(time.Second)
          i++
          if i > 3 {
             break
          }
       }
    }()
    fmt.Println("exit test函数")
}
func main() {
    test()
    time.Sleep(time.Second * 10)
    fmt.Println("exit Main函数")
}结果
额外测试下同时多个子协程使用context退出情况
package main
import (
    "context"
    "fmt"
    "time"
)
func test() {
    go func() { //父协程
       ctx, cancel := context.WithCancel(context.Background())
       defer cancel()
       defer func() {
          fmt.Println("exit 父协程")
       }()
       //子协程
       go child(ctx, "1")
       go child(ctx, "2")
       go child(ctx, "3")
       // 父协程任务
       i := 0
       for {
          fmt.Println("running 父协程")
          time.Sleep(time.Second)
          i++
          if i > 3 {
             break
          }
       }
    }()
    fmt.Println("exit test函数")
}
// 子协程
func child(ctx context.Context, name string) {
    defer func() {
       fmt.Println("exit 子协程 " + name)
    }()
    // 子协程任务
    for {
       select {
       case <-ctx.Done():
          fmt.Println("exit 子协程 " + name + "-收到父协程退出信号")
          return
       default:
          fmt.Println("running 子协程 " + name)
          time.Sleep(time.Second)
       }
    }
}
func main() {
    test()
    time.Sleep(time.Second * 10)
    fmt.Println("exit Main函数")
}由此可知,如果再往下衍生出子协程,同样传入这个context也可以接收到退出信号
特别注意
在编程时特别注意协程和线程的区别,特别是c/c++ java转过来go的,如果忽略了这点,协程可能会耗尽系统的资源

