Go限定协程数量

119 阅读3分钟

不控制协程数量引发的问题

我们都知道Goroutine具备以下两个特点

  • 体积轻量
  • 优质的GMP调度

那么协程数量,能否无限开辟呢,如果做一个服务器或者一些高业务的场景,能否随意的开辟协程数量而不管呢?

先看下面代码:

package main

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

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

    for i := 0; i < task_cnt; i++ {
        go func(i int) {
            //... do some busi...

            fmt.Println("go func ", i, " goroutine count = ", runtime.NumGoroutine())
        }(i)
    }
}

结果: 最后被操作系统以kill信号,强制终结该进程。

所以,我们迅速的开辟gotoutine(不控制并发的goroutine数量)会在短时间内占据操作系统的资源

  • CPU使用率上涨
  • Memory占用不断上涨
  • 主进程崩溃

这些资源实际上是所有用户态程序共享的资源,所以大批的goroutine最终引发的灾难不仅仅是本事,还会关联到其他服务

如何控制协程数量

方法一:用有缓冲的channel来限制

package main

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

func busi(ch chan bool, i int) {

    fmt.Println("go func ", i, " goroutine count = ", runtime.NumGoroutine())
    <-ch
}

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

    ch := make(chan bool, 3)

    for i := 0; i < task_cnt; i++ {

        ch <- true

        go busi(ch, i)
    }

}

结果:

go func  352277  goroutine count =  4
go func  352278  goroutine count =  4
go func  352279  goroutine count =  4
go func  352280  goroutine count =  4
go func  352281  goroutine count =  4
go func  352282  goroutine count =  4
go func  352283  goroutine count =  4
go func  352284  goroutine count =  4
go func  352285  goroutine count =  4

可以看出,协程已经按部就班的执行,并且数量也控制在3个,(4是因为有个main协程)

这种方法其实是取决于for循环的速度,这种方法如果for循环数量较小的情况下,会打印不正确,因为main协程可能会先退出掉,导致其他未结束的协程也结束掉。

方法二:只使用sync同步机制

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

var wg = sync.WaitGroup{}

func busi(i int) {

    fmt.Println("go func ", i, " goroutine count = ", runtime.NumGoroutine())
    wg.Done()
}

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


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

	  wg.Wait()
}


很显然,单纯只使用sync机制,是无法控制协程数量的,所以结果是程序崩溃。

方法三:channel与sync组合使用

package main

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

var wg = sync.WaitGroup{}

func busi(ch chan bool, i int) {

    fmt.Println("go func ", i, " goroutine count = ", runtime.NumGoroutine())

    <-ch

    wg.Done()
}

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

    ch := make(chan bool, 3)

    for i := 0; i < task_cnt; i++ {
		wg.Add(1)

        ch <- true

        go busi(ch, i)
    }

	  wg.Wait()
}


这样程序既可以控制协程的数量,又可以等待所有的协程完成执行后续操作。

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


package main

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

var wg = sync.WaitGroup{}

func busi(ch chan int,i int) {

   for t := range ch {
      fmt.Println("channel index ",i,"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 busi(ch,i)
   }

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

   wg.Wait()
}

这里实际上是将任务的发送和执行做了业务上的分离。使得消息出去,输入SendTask的频率可设置、执行Goroutine的数量也可设置。也就是既控制输入(生产),又控制输出(消费)。使得可控更加灵活。这也是很多Go框架的Worker工作池的最初设计思想理念