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的,如果忽略了这点,协程可能会耗尽系统的资源