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 等。從而導致系統癱瘓甚至影響其他程序。

所以我們開發中一定要重視。

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