Golang无限开启Goroutine?该如何限定Goroutine数量?

2022-10-03
3183

Goroutine;Golang;Go

 一、Goroutine 具备以下两个特点:
  • 体积轻量(占内存小,一个 2kb 左右)
  • 优秀的 GMP 调度

我们如果迅速的开启 goroutine (不控制并发的 goroutine 数量 )的话,会在短时间内占据操作系统的资源(CPU、内存、文件描述符等)。

  • CPU 使用率浮动上涨
  • Memory 占用不断上涨
  • 主进程崩溃(被杀掉了)

二、控制 goroutine 的几种方法
方法一:用有 buffer 的 channel 来限制


package main

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

// 模拟执行业务的 goroutine
// runtime.NumGoroutine()表示获取协程的数量
func doBusiness(ch chan bool, i int) {
  fmt.Println("i的值:", i, "协程数:", runtime.NumGoroutine())
  <-ch
}

func main() {
  max := math.MaxInt64
  fmt.Println(max)

  ch := make(chan bool, 3)

  for i := 0; i < max; i++ {
    ch <- true
    go doBusiness(ch, i)
  }
}


打印的结果:

...
i的值: 101058 协程数: 4
i的值: 101059 协程数: 4
i的值: 101060 协程数: 4
i的值: 101061 协程数: 4
i的值: 101062 协程数: 4
i的值: 101063 协程数: 4
i的值: 101064 协程数: 4
i的值: 101065 协程数: 4
i的值: 101066 协程数: 4
i的值: 101067 协程数: 4
i的值: 101068 协程数: 4
i的值: 101069 协程数: 4
i的值: 101070 协程数: 4
i的值: 101071 协程数: 4
i的值: 101072 协程数: 4
i的值: 101073 协程数: 4
i的值: 101074 协程数: 4
i的值: 101075 协程数: 4
...


从结果看,程序并没有出现崩溃,而是按部就班的顺序执行,并且 go 的数量控制在了 3,(4 的原因是因为还有一个 main goroutine)
但是这段代码有一个小问题,就是如果我们把 go_cnt 的数量变的小一些,会出现打出的结果不正确。

package main

import (
  "fmt"
  "runtime"
)

// 模拟执行业务的 goroutine
// runtime.NumGoroutine()表示获取协程的数量
func doBusiness(ch chan bool, i int) {
  fmt.Println("i的值:", i, "协程数:", runtime.NumGoroutine())
  <-ch
}

func main() {
  //max := math.MaxInt64
  max := 10
  fmt.Println(max)

  ch := make(chan bool, 3)

  for i := 0; i < max; i++ {
    ch <- true
    go doBusiness(ch, i)
  }
}


结果:

10
i的值: 0 协程数: 2
i的值: 1 协程数: 2
i的值: 2 协程数: 2
i的值: 3 协程数: 2
i的值: 4 协程数: 2
i的值: 5 协程数: 2
i的值: 6 协程数: 2
i的值: 7 协程数: 2
i的值: 8 协程数: 2




可以从上面的实例中看出来有些 goroutine 没有打印出来,是由于 main 把所有 goroutine 开启之后,main 就直接退出了,我们知道 main 进程退出,低下所有的 goroutine 都会结束掉,从而导致有些 goroutine 还没来得及执行就退出了。所以想全部 go 都执行,需要在 main 的最后进行阻塞操作。
方法二:使用 sync 同步机制

package main

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

var wg = sync.WaitGroup{}

func doBusiness(i int) {

  fmt.Println("i的值 ", i, " 协程的数量为 = ", runtime.NumGoroutine())
  wg.Done()
}

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

  for i := 0; i < max; i++ {
    wg.Add(1)
    go doBusiness(i)
  }

  wg.Wait()
}


很明显,如果单纯的使用 sync 也达不到控制 goroutine 的数量,最终结果依然是崩溃。

方法三:channel 与 sync 同步组合方式实现控制 goroutine

package main

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

var wg = sync.WaitGroup{}

func doBusiness(ch chan bool, i int) {

  fmt.Println("i的值为 ", i, " 协程数量为 = ", runtime.NumGoroutine())

  <-ch

  wg.Done()
}

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

  ch := make(chan bool, 3)

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

  wg.Wait()
}



方法四:利用无缓冲 channel 与任务发送/执行分离方式
package main

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

var wg = sync.WaitGroup{}

func doBusiness(ch chan int) {

  for t := range ch {
    fmt.Println("go task = ", t, ", goroutine count = ", runtime.NumGoroutine())
    wg.Done()
  }
}

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

func main() {

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

  goCnt := 3 //启动goroutine的数量
  for i := 0; i < goCnt; i++ {
    //启动go
    go doBusiness(ch)
  }

  taskCnt := math.MaxInt64 //模拟用户需求业务的数量
  for t := 0; t < taskCnt; t++ {
    //发送任务
    sendTask(t, ch)
  }

  wg.Wait()
}