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

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

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

package main
  
import (
        "fmt"
        "math"
        "runtime"
)


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

程序运行在中途主进程直接被操作系统杀死,如下图所示:
请添加图片描述

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

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

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

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

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

3.1 只用有buffer的channel

例如使用一个有缓冲的channel。当channel满了的时候,其会发生阻塞,避免一直不断的开辟goroutine。其设计逻辑如下:
请添加图片描述
完整代码如下:

package main
  
import (
        "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
        // 创建一个带缓冲的channel
        myChan := make(chan bool,3)

        // 循环创建业务
        for i := 0 ; i< task_cnt;i++ {
                myChan <- true
                go  MyWork(myChan,i)
        }
}

按照上面的方式使得能够一直运行。其实实际上,执行的只有3个(还有一个main goroutine)。上面代码的本质就是在myChan <- true处会阻塞,直到之前三个中有一个完成了任务,阻塞接触,才开辟一个新的goroutine。
请添加图片描述

3.2 channel与sync同步组合方式

  • 如果我们只使用sync的WaitGroup会怎么样?
    package main
      
    import (
            "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()
    }
    
    
    结果是仍然无法大量开辟,主线程会被操作系统杀死。
    请添加图片描述
  • channel与sync同步组合方式
    package main
    
    import (
            "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
            // 创建一个带缓冲的channel
            myChan := make(chan bool,3)
    
            // 循环创建业务
            for i := 0 ; i< task_cnt;i++ {
                    wg.Add(1)
                    myChan <- true
                    go  MyWork(myChan,i)
            }
            wg.Wait()
    }
    

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

代码逻辑:

package main

import (
	"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()
}

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值