文章目录

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

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工作池的最初设计思想理念。

精彩内容

评论可见,请评论后查看内容,谢谢!!!
 您阅读本篇文章共花了: