> 文章列表 > Golang中是否可以无限开辟协程以及如何控制协程的数量?

Golang中是否可以无限开辟协程以及如何控制协程的数量?

Golang中是否可以无限开辟协程以及如何控制协程的数量?

文章目录

  • 1. Golang中是否可以无限开辟协程?
  • 2. 不控制goroutine数量引发的问题
  • 3. 如何控制goroutine的数量?⭐️
    • 3.1 只用有buffer的channel
    • 3.2 channel与sync同步组合方式
    • 3.3 利用无缓冲channel与任务发送/执行分离方式

1. Golang中是否可以无限开辟协程?

首先我们在linux操作系统上运行以下这段程序,看会发生什么?

package mainimport ("fmt""math""runtime"
)// 测试是否可以无限go
func main(){// 模式业务需要开辟的数量task_cnt := math.MaxInt64for i := 0 ; i< task_cnt;i++ {go func (num int){// 完成一些业务fmt.Println("go func ",i," goroutine count=",runtime.NumGoroutine())}(i)}
}

程序运行在中途主进程直接被操作系统杀死,如下图所示:
Golang中是否可以无限开辟协程以及如何控制协程的数量?

2. 不控制goroutine数量引发的问题

我们知道goroutine具备轻量高效GPM调度的特点,如果无限开辟goroutine,短时间内会占用大量的占用操作系统的资源(文件描述符、CPU、内存等):

  • CPU浮动上涨;
  • 内存占用持续身高;
  • 主进程被操作系统杀死;

这些资源实际上是用户态程序共享的资源,所以大批的goroutine最终引发灾难不仅仅是自身,还会关联其他运行的程序。

3. 如何控制goroutine的数量?⭐️

3.1 只用有buffer的channel

例如使用一个有缓冲的channel。当channel满了的时候,其会发生阻塞,避免一直不断的开辟goroutine。其设计逻辑如下:
Golang中是否可以无限开辟协程以及如何控制协程的数量?
完整代码如下:

package mainimport ("fmt""math""runtime"
)func MyWork(c chan bool,i int){fmt.Println("go func ",i," goroutine count=",runtime.NumGoroutine())<- c
}// 测试是否可以无限go
func main(){// 模式业务需要开辟的数量task_cnt := math.MaxInt64// 创建一个带缓冲的channelmyChan := make(chan bool,3)// 循环创建业务for i := 0 ; i< task_cnt;i++ {myChan <- truego  MyWork(myChan,i)}
}

按照上面的方式使得能够一直运行。其实实际上,执行的只有3个(还有一个main goroutine)。上面代码的本质就是在myChan <- true处会阻塞,直到之前三个中有一个完成了任务,阻塞接触,才开辟一个新的goroutine。
Golang中是否可以无限开辟协程以及如何控制协程的数量?

3.2 channel与sync同步组合方式

  • 如果我们只使用sync的WaitGroup会怎么样?
    package mainimport ("fmt""math""runtime""sync"
    )// 创建一个全局的wait_group{}
    var wg = sync.WaitGroup{}func MyWork(i int){fmt.Println("go func ",i," goroutine count=",runtime.NumGoroutine())wg.Done()
    }// 测试是否可以无限go
    func main(){// 模式业务需要开辟的数量task_cnt := math.MaxInt64// 循环创建业务for i := 0 ; i< task_cnt;i++ {wg.Add(1)go  MyWork(i)}// 阻塞等待wg.Wait()
    }

    结果是仍然无法大量开辟,主线程会被操作系统杀死。
    Golang中是否可以无限开辟协程以及如何控制协程的数量?

  • channel与sync同步组合方式
    package mainimport ("fmt""math""runtime""sync"
    )// 创建一个sync.WaitGroup{}变量
    var wg = sync.WaitGroup{}func MyWork(c chan bool,i int){fmt.Println("go func ",i," goroutine count=",runtime.NumGoroutine())wg.Done()<- c
    }// 测试是否可以无限go
    func main(){// 模式业务需要开辟的数量task_cnt := math.MaxInt64// 创建一个带缓冲的channelmyChan := make(chan bool,3)// 循环创建业务for i := 0 ; i< task_cnt;i++ {wg.Add(1)myChan <- truego  MyWork(myChan,i)}wg.Wait()
    }
    

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

代码逻辑:

package mainimport ("fmt""math""runtime""sync"
)// 定义一个WaitGroup类型的变量,保证所有的任务都能执行完毕
var wg = sync.WaitGroup{}// 任务执行函数
func MyWork(c chan int){// 表示业务执行完毕defer wg.Done()for t := range c {// 模拟业务处理逻辑fmt.Println(t)}
}// 发送业务的函数
func SendTask(c chan int,task int){// 保证所有的任务都能执行完毕wg.Add(1)c <- task
}func main(){// 创建一个无缓冲的通道myChan := make(chan int)// 开辟固定数量的协程for i := 0; i < 3 ; i++ {go MyWork(myChan) // 他们都会各自内部阻塞,等待任务发送过来}// 最大任务数量task_cnt := math.MaxInt64// 开始发送任务for i := 0 ; i < task_cnt ; i++ {SendTask(myChan,i)}// 等待wg.Wait()
}

整体架构如下
Golang中是否可以无限开辟协程以及如何控制协程的数量?
这里实际上是将任务的发送和执行做了业务上的分离。使得输入SendTask的频率可设置、执行Goroutine的数量也可设置。也就是既控制输入(生产),又控制输出(消费)。使得可控更加灵活。这也是很多Go框架的Worker工作池的最初设计思想理念。