文章目录
- 1. Golang中是否可以无限开辟协程?
- 2. 不控制goroutine数量引发的问题
- 3. 如何控制goroutine的数量?⭐️
- 3.1 只用有buffer的channel
- 3.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工作池的最初设计思想理念。