GO语言中协程中的协程不会退出的 "陷阱"

来源:赵克立博客 分类: Go 标签:GOGO语法发布时间:2023-08-14 10:08:50最后更新:2023-08-15 14:06:46浏览:171
版权声明:
本文为博主原创文章,转载请声明原文链接...谢谢。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函数")
}

运行结果

2308141691979223991822.png

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函数")
}

结果

2308141691980756982055.png

额外测试下同时多个子协程使用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函数")
}

2308141691981486303898.png

由此可知,如果再往下衍生出子协程,同样传入这个context也可以接收到退出信号

特别注意

在编程时特别注意协程和线程的区别,特别是c/c++  java转过来go的,如果忽略了这点,协程可能会耗尽系统的资源


微信号:kelicom QQ群:215861553 紧急求助须知
Win32/PHP/JS/Android/Python