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

