Golang 如何有效限制併發數?
Go 語言目前很火熱,一部分原因在於自身帶 “高併發” 的標籤,其本身就擁有優秀的併發量和吞吐量。
1 協程可以無限創建嗎?
我們在日常開發中會有高併發場景,有時會用多協程併發實現。在高併發業務場景,能否可以隨意開闢 goroutine 並且放養不管呢?畢竟有強大的 GC 和優越的 GMP 調度算法。
看下面的代碼:
package main
import (
"fmt"
"math"
"runtime"
)
func main() {
taskCount := math.MaxInt64
for i := 0; i < taskCount; i++ {
go func(i int) {
fmt.Println("go func ", i, " goroutine count = ", runtime.NumGoroutine())
}(i)
}
}
運行結果:
結果可以看到,程序最終會被系統強制 kill 掉,強制結束進程。
如果我們大量的開啓 goroutine 會佔滿某一時間操作系統上用戶態程序共享的資源,其中包括 CPU、Memory、Fd 等。從而導致系統癱瘓甚至影響其他程序。
-
CPU 使用率瞬間上漲
-
Memory 佔用不斷上漲
-
主進程崩潰,強制 Kill
所以我們開發中一定要重視。
2 如何控制 goroutine 數量
2.1 通過 buffer channl 來控制 goroutine
package main
import (
"fmt"
"runtime"
)
func work(ch chan bool, i int) {
fmt.Println("go func ", i, " goroutine count = ", runtime.NumGoroutine())
<-ch
}
func main() {
taskCount := 10
ch := make(chan bool, 3)
for i := 0; i < taskCount; i++ {
ch <- true
go work(ch, i)
}
}
程序運行結果:
解讀下代碼,這裏我們用了 3 個 channel 對應 3 個 goroutine 執行任務。在同一時間內運行的 goroutine 的數量與 channel 限制 buffer 的數量是一致的,從而達到限制 goroutine 的效果。
2.2 通過 sync.WaitGroup 來控制 goroutine
package main
import (
"fmt"
"math"
"sync"
"runtime"
)
var wg = sync.WaitGroup{}
func work(i int) {
fmt.Println("go func ", i, " goroutine count = ", runtime.NumGoroutine())
wg.Done()
}
func main() {
//模擬用戶需求業務的數量
taskCount := math.MaxInt64
for i := 0; i < taskCount; i++ {
wg.Add(1)
go work(i)
}
wg.Wait()
}
運行結果:
從運行結果可以看出,進程還是被操作系統強制 Kill 了,使用 sync.WaitGroup{} 並不能控制 goroutine 的數量。
2.3 channel & sync.WaitGroup 同步組合方式
package main
import (
"fmt"
"math"
"sync"
"runtime"
)
var wg = sync.WaitGroup{}
func work(ch chan bool, i int) {
fmt.Println("go func ", i, " goroutine count = ", runtime.NumGoroutine())
<-ch
wg.Done()
}
func main() {
//模擬用戶需求go業務的數量
taskCount := math.MaxInt64
ch := make(chan bool, 3)
for i := 0; i < taskCount; i++ {
wg.Add(1)
ch <- true
go work(ch, i)
}
wg.Wait()
}
運行結果:
進程沒有被操作系統 Kill,通過 buffer channel 這種控制住了 goroutine 數量。
2.4 無 buffer channel 控制 goroutine 數量
package main
import (
"fmt"
"sync"
"runtime"
)
var wg = sync.WaitGroup{}
func work(ch chan int) {
for i := range ch {
fmt.Println("go func ", i, " goroutine count = ", runtime.NumGoroutine())
wg.Done()
}
}
func sendTask(task int, ch chan int) {
wg.Add(1)
ch <- task
}
func main() {
// 無 buffer channel
ch := make(chan int)
goCount := 3
for i := 0; i < goCount; i++ {
// 啓動go
go busi(ch)
}
taskCount := 10
for t := 0; t < taskCount; t++ {
// 發送任務
sendTask(t, ch)
}
wg.Wait()
}
運行結果:
首先創建了無 buffer 的 channel,將任務發送到 channel 中,通過控制 goroutine 數量的方式執行程序,達到控制 goroutine。
2.5 協程池方式控制 goroutine
線程過多會帶來調度開銷,進而影響緩存局部性和整體性能。而線程池維護着多個線程,等待着監督管理者分配可併發執行的任務。使用線程池避免了在處理短時間任務時創建和銷燬線程的代價。
字節跳動庫:
https://github.com/bytedance/gopkg/tree/develop/util/gopool
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/t5QlZcRqwegkKnj8B4_7yg