如何控制go 协程的创建

发布于 2024-05-26  442 次阅读


1.背景

一次在实际开发过程中,由于在for循环中不断创建 go协程,导致当go协程数量达到一定的数量时候程序直接被系统kill掉了。因此本文就想探究一下在go中是否能无限创建go协程,如果不能,那么能创建最大的协程数量是多少。

2. 协程可以无限创建吗?

首先我们可以在for循环中不断创建go协程看看能创建多少个go协程后进程被kill



import (
   "fmt"
   "math"
   "runtime"
)

func main() {
   taskCount := math.MaxInt64

   for i := 0; i < taskCount; i++ {
       go func(i int) {
           fmt.Println("go func ", i, " goroutine count = ", runtime.NumGoroutine())
      }(i)
  }
}

运行结果:

结果可以看到,当协程最大数量达到29900时候程序最终会被系统强制 kill 掉,强制结束进程。

如果我们大量的开启 goroutine 会占满某一时间操作系统上用户态程序共享的资源,其中包括 CPU、Memory、Fd 等。从而导致系统瘫痪甚至影响其他程序。

  • CPU 使用率瞬间上涨
  • Memory 占用不断上涨
  • 主进程崩溃,强制 Kill
  • 不同机器的能够创建的最大协程数量是不一样的

3 如何控制 goroutine 数量

3.1 通过 buffer channl 来控制 goroutine



import (
   "fmt"
   "runtime"
)

func work(ch chan bool, i int) {
   fmt.Println("go func ", i, " goroutine count = ", runtime.NumGoroutine())
   <-ch
}

func main() {
   taskCount := 10
   
   ch := make(chan bool, 3)
   for i := 0; i < taskCount; i++ {
       ch <- true
       go work(ch, i)
  }
}

程序运行结果:

解读下代码,这里我们用了 3个 channel 对应 3 个 goroutine 执行任务。在同一时间内运行的 goroutine 的数量与 channel 限制 buffer 的数量是一致的,从而达到限制 goroutine 的效果。

3.2 通过 sync.WaitGroup 来控制 goroutine


import (
   "fmt"
   "math"
   "sync"
   "runtime"
)

运行结果:

从运行结果可以看出,进程还是被操作系统强制 Kill 了,使用 sync.WaitGroup{} 并不能控制 goroutine 的数量。

3.3 channel & sync.WaitGroup 同步组合方式



import (
   "fmt"
   "math"
   "sync"
   "runtime"
)

var wg = sync.WaitGroup{}

func work(ch chan bool, i int) {
   fmt.Println("go func ", i, " goroutine count = ", runtime.NumGoroutine())
   <-ch

   wg.Done()
}

func main() {
   //模拟用户需求go业务的数量
   taskCount := math.MaxInt64

   ch := make(chan bool, 3)

   for i := 0; i < taskCount; i++ {
wg.Add(1)
       ch <- true
       go work(ch, i)
  }

 wg.Wait()
}

运行结果:

进程没有被操作系统 Kill,通过 buffer channel 这种控制住了 goroutine 数量。

3.4 无 buffer channel 控制 goroutine 数量



import (
   "fmt"
   "sync"
   "runtime"
)
var wg = sync.WaitGroup{}

func work(ch chan int) {
   for i := range ch {
       fmt.Println("go func ", i, " goroutine count = ", runtime.NumGoroutine())
       wg.Done()
  }
}

func sendTask(task int, ch chan int) {
   wg.Add(1)
   ch <- task
}

func main() {
   // 无 buffer channel
   ch := make(chan int)  

   goCount := 3              
   for i := 0; i < goCount; i++ {
       // 启动go
       go busi(ch)
  }

   taskCount := 10
   for t := 0; t < taskCount; t++ {
       // 发送任务
       sendTask(t, ch)
  }

wg.Wait()
}

运行结果:

首先创建了无 buffer 的 channel,将任务发送到 channel 中,通过控制 goroutine 数量的方式执行程序,达到控制 goroutine。

3.5 协程池方式控制 goroutine

如果对go协程的创建数量不加以限制,那么最终会导致创建的go协程数量膨胀,CPU负载过高,内存和文件描述符fd占用过多导致进程最终出现异常停止,因此我们可以借助协程池去管理控制go协程的数量。可以参考:https://github.com/bytedance/gopkg/tree/develop/util/gopool

4.总结

从以上可以知道,在实际开发过程中,对于轻量级的go协程并发,我们也需要注意控制go协程的数量,避免对我们的服务造成影响。


繁华落尽,雪花漫天飞舞。