Go 语言目前很火热,一部分原因在于自身带"高并发"的标签,其本身就拥有及其优秀的并发量和吞吐量。
1 协程可以无限创建吗?
我们在日常开发中会有高并发场景,有时会用多协程并发实现。在高并发业务场景,能否可以随意开辟 goroutine 并且放养不管呢?毕竟有强大的 GC 和优越的 GMP 调度算法。
看下面的代码:
go
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
go
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
go
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 同步组合方式
go
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 数量
go
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
线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。使用线程池避免了在处理短时间任务时创建和销毁线程的代价。
字节跳动: