[TOC]
## 4、Go是否可以無限go? 如何限定數量?
### 一、不控制goroutine數量引發的問題
我們都知道Goroutine具備如下兩個特點
* 體積輕量
* 優質的GMP調度
那么goroutine是否可以無限開辟呢,如果做一個服務器或者一些高業務的場景,能否隨意的開辟goroutine并且放養不管呢?讓他們自生自滅,畢竟有強大的GC和優質的調度算法支撐?
那么我可以先看如下一個問題。
> code1.go
```go
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)
}
}
```
結果
<img src="./pic/147-goroutines1.png" alt="image-20200328231947588" style="zoom:50%;" />
最后被操作系統以kill信號,強制終結該進程。
```bash
signal: killed
```
所以,我們迅速的開辟goroutine(**不控制并發的 goroutine 數量** )會在短時間內占據操作系統的資源(CPU、內存、文件描述符等)。
- CPU 使用率浮動上漲
- Memory 占用不斷上漲。
- 主進程崩潰(被殺掉了)
這些資源實際上是所有用戶態程序共享的資源,所以大批的goroutine最終引發的災難不僅僅是自身,還會關聯其他運行的程序。
所以在編寫邏輯業務的時候,限制goroutine是我們必須要重視的問題。
---
### 二、一些簡單方法控制goroutines數量
#### 方法一:只是用有buffer的channel來限制
> code2.go
```go
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)
}
}
```
結果
```bash
...
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
go func 352286 goroutine count = 4
go func 352287 goroutine count = 4
go func 352288 goroutine count = 4
go func 352289 goroutine count = 4
go func 352290 goroutine count = 4
go func 352291 goroutine count = 4
go func 352292 goroutine count = 4
go func 352293 goroutine count = 4
go func 352294 goroutine count = 4
go func 352295 goroutine count = 4
go func 352296 goroutine count = 4
go func 352297 goroutine count = 4
go func 352298 goroutine count = 4
go func 352299 goroutine count = 4
go func 352300 goroutine count = 4
go func 352301 goroutine count = 4
go func 352302 goroutine count = 4
...
```
從結果看,程序并沒有出現崩潰,而是按部就班的順序執行,并且go的數量控制在了3,(4的原因是因為還有一個main goroutine)那么從數字上看,是不是在跑的goroutines有幾十萬個呢?

這里我們用了,buffer為3的channel, 在寫的過程中,實際上是限制了速度。限制的是
```go
for i := 0; i < go_cnt; i++ { //循環速度
ch <- true
go busi(ch, i)
}
```
`for`循環的速度,因為這個速度決定了go的創建速度,而go的結束速度取決于 `busi()`函數的執行速度。 這樣實際上,我們就能夠保證了,同一時間內運行的goroutine的數量與buffer的數量一致。從而達到了限定效果。
但是這段代碼有一個小問題,就是如果我們把go_cnt的數量變的小一些,會出現打出的結果不正確。
```go
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)
}
}
```
結果
```bash
go func 2 goroutine count = 4
go func 3 goroutine count = 4
go func 4 goroutine count = 4
go func 5 goroutine count = 4
go func 6 goroutine count = 4
go func 1 goroutine count = 4
go func 8 goroutine count = 4
```
是因為`main`將全部的go開辟完之后,就立刻退出進程了。所以想全部go都執行,需要在main的最后進行阻塞操作。
#### 方法二:只使用sync同步機制
> code3.go
```go
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`依然達不到控制goroutine的數量,所以最終結果依然是崩潰。
結果
```bash
...
go func 7562 goroutine count = 7582
go func 24819 goroutine count = 17985
go func 7685 goroutine count = 7582
go func 24701 goroutine count = 17984
go func 7563 goroutine count = 7582
go func 24821 goroutine count = 17983
go func 24822 goroutine count = 17983
go func 7686 goroutine count = 7582
go func 24703 goroutine count = 17982
go func 7564 goroutine count = 7582
go func 24824 goroutine count = 17981
go func 7687 goroutine count = 7582
go func 24705 goroutine count = 17980
go func 24706 goroutine count = 17980
go func 24707 goroutine count = 17979
go func 7688 goroutine count = 7582
go func 24826 goroutine count = 17978
go func 7566 goroutine count = 7582
go func 24709 goroutine count = 17977
go func 7689 goroutine count = 7582
go func 24828 goroutine count = 17976
go func 24829 goroutine count = 17976
go func 7567 goroutine count = 7582
go func 24711 goroutine count = 17975
//操作系統停止響應
```
#### 方法三:channel與sync同步組合方式
> code4.go
```go
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()
}
```
結果
```bash
//...
go func 228851 goroutine count = 4
go func 228852 goroutine count = 4
go func 228853 goroutine count = 4
go func 228854 goroutine count = 4
go func 228855 goroutine count = 4
go func 228856 goroutine count = 4
go func 228857 goroutine count = 4
go func 228858 goroutine count = 4
go func 228859 goroutine count = 4
go func 228860 goroutine count = 4
go func 228861 goroutine count = 4
go func 228862 goroutine count = 4
go func 228863 goroutine count = 4
go func 228864 goroutine count = 4
go func 228865 goroutine count = 4
go func 228866 goroutine count = 4
go func 228867 goroutine count = 4
//...
```
這樣我們程序就不會再造成資源爆炸而崩潰。而且運行go的數量控制住了在buffer為3的這個范圍內。
#### 方法四:利用無緩沖channel與任務發送/執行分離方式
> code5.go
```go
package main
import (
"fmt"
"math"
"sync"
"runtime"
)
var wg = sync.WaitGroup{}
func busi(ch chan int) {
for t := range ch {
fmt.Println("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)
}
taskCnt := math.MaxInt64 //模擬用戶需求業務的數量
for t := 0; t < taskCnt; t++ {
//發送任務
sendTask(t, ch)
}
wg.Wait()
}
```
結構
```bash
//...
go task = 130069 , goroutine count = 4
go task = 130070 , goroutine count = 4
go task = 130071 , goroutine count = 4
go task = 130072 , goroutine count = 4
go task = 130073 , goroutine count = 4
go task = 130074 , goroutine count = 4
go task = 130075 , goroutine count = 4
go task = 130076 , goroutine count = 4
go task = 130077 , goroutine count = 4
go task = 130078 , goroutine count = 4
go task = 130079 , goroutine count = 4
go task = 130080 , goroutine count = 4
go task = 130081 , goroutine count = 4
go task = 130082 , goroutine count = 4
go task = 130083 , goroutine count = 4
go task = 130084 , goroutine count = 4
go task = 130085 , goroutine count = 4
go task = 130086 , goroutine count = 4
go task = 130087 , goroutine count = 4
go task = 130088 , goroutine count = 4
go task = 130089 , goroutine count = 4
go task = 130090 , goroutine count = 4
go task = 130091 , goroutine count = 4
go task = 130092 , goroutine count = 4
go task = 130093 , goroutine count = 4
...
```
執行流程大致如下,這里實際上是將任務的發送和執行做了業務上的分離。使得消息出去,輸入SendTask的頻率可設置、執行Goroutine的數量也可設置。也就是既控制輸入(生產),又控制輸出(消費)。使得可控更加靈活。這也是很多Go框架的Worker工作池的最初設計思想理念。

---
以上便是目前有關限定goroutine基礎設計思路。
參考:
http://team.jiunile.com/blog/2019/09/go-control-goroutine-number.html
https://www.joyk.com/dig/detail/1547976674512705
- 封面
- 第一篇:Golang修養必經之路
- 1、最常用的調試 golang 的 bug 以及性能問題的實踐方法?
- 2、Golang的協程調度器原理及GMP設計思想?
- 3、Golang中逃逸現象, 變量“何時棧?何時堆?”
- 4、Golang中make與new有何區別?
- 5、Golang三色標記+混合寫屏障GC模式全分析
- 6、面向對象的編程思維理解interface
- 7、Golang中的Defer必掌握的7知識點
- 8、精通Golang項目依賴Go modules
- 9、一站式精通Golang內存管理
- 第二篇:Golang面試之路
- 1、數據定義
- 2、數組和切片
- 3、Map
- 4、interface
- 5、channel
- 6、WaitGroup
- 第三篇、Golang編程設計與通用之路
- 1、流?I/O操作?阻塞?epoll?
- 2、分布式從ACID、CAP、BASE的理論推進
- 3、對于操作系統而言進程、線程以及Goroutine協程的區別
- 4、Go是否可以無限go? 如何限定數量?
- 5、單點Server的N種并發模型匯總
- 6、TCP中TIME_WAIT狀態意義詳解
- 7、動態保活Worker工作池設計