Go如何有效控制Goroutine并發(fā)數(shù)量

相信大家在學習Go的過程中,都會看到類似這樣一句話:"與傳統(tǒng)的系統(tǒng)級線程和進程相比,協(xié)程的最大優(yōu)勢在于其‘輕量級’,可以輕松創(chuàng)建上百萬個而不會導致系統(tǒng)資源衰竭"。那是不是意味著我們在開發(fā)過程中,可以隨心所欲的調(diào)用協(xié)程,而不關(guān)心它的數(shù)量呢?

答案當然是否定的。我們在開發(fā)過程中,如果不對Goroutine加以控制而進行濫用的話,可能會導致服務程序整體崩潰。

這里我先模擬一下協(xié)程數(shù)量太多的危害:

func main() {
  number := math.MaxInt64
  for i := 0; i < number; i++ {
    go func(i int) {
      // 做一些業(yè)務邏輯處理
      fmt.Printf("go func: %d\n", i)
      time.Sleep(time.Second)
    }(i)
  }
}

如果number是用戶輸入的一個參數(shù),沒有做限制。有些開發(fā)人員會全部丟進去進行循環(huán),認為全部都并發(fā)使用Goroutine去做一件事情,效率比較高。但這樣的話,噩夢般的事情就開始了,服務器系統(tǒng)資源利用率不斷上漲,到最后程序自動killed。

image.png

通過執(zhí)行top命令查看到該程序占用的CPU、內(nèi)存較高。

image.png

為了避免上圖這種情況,下面會簡單的介紹一下Goroutine以及在我們?nèi)粘i_發(fā)中如何控制Goroutine的數(shù)量。

一、基本介紹

工欲善其事必先利其器。先簡單的介紹一下Goroutine,Goroutine是Go中最基本的執(zhí)行單元。事實上每一個Go程序至少有一個Goroutine:主Goroutine。當程序啟動時,它會自動創(chuàng)建。

為了更好理解Goroutine,先講一下進程、線程和協(xié)程的概念。

進程(process):用戶下達運行程序的命令后,就會產(chǎn)生進程。同一程序可產(chǎn)生多個進程(一對多關(guān)系),以允許同時有多位用戶運行同一程序,卻不會相沖突。進程需要一些資源才能完成工作,如CPU使用時間、存儲器、文件以及I/O設備,且為依序逐一進行,也就是每個CPU核心任何時間內(nèi)僅能運行一項進程。進程的局限是創(chuàng)建、撤銷和切換的開銷比較大。

線程(Thread):有時被稱為輕量級進程(Lightweight Process,LWP),是程序執(zhí)行流的最小單元。一個標準的線程由線程ID,當前指令指針(PC),寄存器集合和堆棧組成。另外,線程是進程中的一個實體,是被系統(tǒng)獨立調(diào)度和分派的基本單位,線程自己不擁有系統(tǒng)資源,只擁有一點兒在運行中必不可少的資源,但它可與同屬一個進程的其它線程共享進程所擁有的全部資源。線程擁有自己獨立的棧和共享的堆,共享堆,不共享棧,線程的切換一般也由操作系統(tǒng)調(diào)度。

協(xié)程(coroutine):又稱微線程與子例程(或者稱為函數(shù))一樣,協(xié)程(coroutine)也是一種程序組件。相對子例程而言,協(xié)程更為一般和靈活,但在實踐中使用沒有子例程那樣廣泛。和線程類似,共享堆,不共享棧,協(xié)程的切換一般由程序員在代碼中顯式控制。它避免了上下文切換的額外耗費,兼顧了多線程的優(yōu)點,簡化了高并發(fā)程序的復雜。

Goroutine和其他語言的協(xié)程(coroutine)在使用方式上類似,但從字面意義上來看不同(一個是Goroutine,一個是coroutine),再就是協(xié)程是一種協(xié)作任務控制機制,在最簡單的意義上,協(xié)程不是并發(fā)的,而Goroutine支持并發(fā)的。因此Goroutine可以理解為一種Go語言的協(xié)程,同時它可以運行在一個或多個線程上。

在Go中生成一個Goroutine的方式非常的簡單:只要在函數(shù)前面加上go就生成了。

 func number() {
    for i := 0; i < ; i++ {
        fmt.Printf("%d ", i)
    }
}
func main() {
   go number() // 啟動一個goroutine
   number()
}

二、協(xié)程池解決?

回到開頭的問題,如何控制Goroutine的數(shù)量?相信有過開發(fā)經(jīng)驗的人,第一想法是生成協(xié)程池,通過協(xié)程池控制連接的數(shù)量,這樣每次連接都從協(xié)程池里去拿。在Golang開發(fā)中需要協(xié)程池嗎?這里分享下知乎有個相關(guān)點贊最高的回答:

顯然不需要,goroutine的初衷就是輕量級的線程,為的就是讓你隨用隨起,結(jié)果你又搞個池子來,這不是脫褲子放屁么?你需要的是限制并發(fā),而協(xié)程池是一種違背了初衷的方法。池化要解決的問題一個是頻繁創(chuàng)建的開銷,另一個是在等待時占用的資源。goroutine 和普通線程相比,創(chuàng)建和調(diào)度都不需要進入內(nèi)核,也就是創(chuàng)建的開銷已經(jīng)解決了。同時相比系統(tǒng)線程,內(nèi)存占用也是輕量的。所以池化技術(shù)要解決的問題goroutine 都不存在,為什么要創(chuàng)建 goroutine pool 呢?如果因為 goroutine 持有資源而要去創(chuàng)建goroutine pool,那只能說明代碼的耦合度較高,應該為這類資源創(chuàng)建一個goroutine-safe的對象池,而不是把goroutine本身池化。

在我們?nèi)粘4蟛糠謭鼍跋?,不需要使用協(xié)程池。因為Goroutine非常輕量,默認2kb,使用go func()很難成為性能瓶頸。當然一些極端情況下需要追求性能,可以使用協(xié)程池實現(xiàn)資源的復用,例如FastHttp使用協(xié)程池性能提高許多。

當然現(xiàn)在我們?nèi)绻枰褂肎oroutine池也不需要重復造輪子了,目前github上已經(jīng)有開源的項目ants來實現(xiàn) Goroutine 池。ants已經(jīng)實現(xiàn)了對大規(guī)模 Goroutine 的調(diào)度管理、Goroutine 復用,允許使用者在開發(fā)并發(fā)程序的時候限制 Goroutine 數(shù)量,復用資源,達到更高效執(zhí)行任務的效果。

項目地址:https://github.com/panjf2000/ants

三、 通過channel和sync方式限制協(xié)程數(shù)量

3.1 Channel

Goroutine運行在相同的地址空間,因此訪問共享內(nèi)存必須做好同步。那么Goroutine之間如何進行數(shù)據(jù)的通信呢?Go提供了一個很好的通信機制channel,channel可以與 Unix shell 中的雙向管道做類比:可以通過它發(fā)送或者接收值。這些值只能是特定的類型:channel類型。定義一個channel時,也需要定義發(fā)送到channel的值的類型。注意,必須使用make創(chuàng)建channel。

3.2 Sync

Go語言中有一個sync.WaitGroup,WaitGroup 對象內(nèi)部有一個計數(shù)器,最初從0開始,它有三個方法:Add(), Done(), Wait() 用來控制計數(shù)器的數(shù)量。下面示例代碼中wg.Wati會阻塞代碼的運行,直到計數(shù)器值為0。

通過Golang自帶的channel和sync,可以實現(xiàn)需求,下面代碼中通過channel控制Goroutine數(shù)量。

 package main
import (
  "fmt"
  "sync"
  "time"
)
type Glimit struct {
  n int
  c chan struct{}
}
// initialization Glimit struct
func New(n int) *Glimit {
  return &Glimit{
    n: n,
    c: make(chan struct{}, n),
  }
}
// Run f in a new goroutine but with limit.
func (g *Glimit) Run(f func()) {
  g.c <- struct{}{}
  go func() {
    f()
    <-g.c
  }()
}
var wg = sync.WaitGroup{}
func main() {
  number := 10
  g := New(2)
  for i := 0; i < number; i++ {
    wg.Add(1)
    value :=i
    goFunc := func() {
      // 做一些業(yè)務邏輯處理
      fmt.Printf("go func: %d\n", value)
      time.Sleep(time.Second)
      wg.Done()
    }
    g.Run(goFunc)
  }
  wg.Wait()
}

四、總結(jié)

在文章的開頭通過在服務器模擬Goroutine數(shù)量太多導致系統(tǒng)資源上升,提醒大家避免這類問題。當然每個人可根據(jù)自己所在的場景選擇最合適的方案,有時候成熟的第三方庫也是個很好的選擇,可以避免重復造輪子。

下面有兩個思考問題,大家可以嘗試著去思考一下。

思考1:為什么我們要使用sync.WaitGroup?

這里如果我們不使用sync.WaitGroup控制的話,原因出在當主程序結(jié)束時,子協(xié)程也是會被終止掉的。因此剩余的 goroutine 沒來及把值輸出,程序就已經(jīng)中斷了

思考2:代碼中channel數(shù)據(jù)結(jié)構(gòu)為什么定義struct,而不定義成bool這種類型呢?

因為空結(jié)構(gòu)體變量的內(nèi)存占用大小為0,而bool類型內(nèi)存占用大小為1,這樣可以更加最大化利用我們服務器的內(nèi)存空間。

func main(){
  a :=struct{}{}
  b := true
  fmt.Println(unsafe.Sizeof(a))  # println 0
  fmt.Println(unsafe.Sizeof(b))  # println 1
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

  • 原文地址:來,控制一下 Goroutine 的并發(fā)數(shù)量 問題 在這里,假設 userCount 是一個外部傳入的參...
    EDDYCJY閱讀 3,100評論 1 10
  • go并發(fā)編程入門到放棄 并發(fā)和并行 并發(fā):一個處理器同時處理多個任務。 并行:多個處理器或者是多核的處理器同時處理...
    yangyunfeng閱讀 648評論 0 2
  • Channel 是什么? channel,通道,本質(zhì)上是一個通信對象,goroutine 之間可以使用它來通信。從...
    癩痢頭閱讀 1,119評論 0 0
  • Go語言中的并發(fā)編程 并發(fā)是編程里面一個非常重要的概念,Go語言在語言層面天生支持并發(fā),這也是Go語言流行的一個很...
    吳佳浩閱讀 405評論 0 1
  • 16宿命:用概率思維提高你的勝算 以前的我是風險厭惡者,不喜歡去冒險,但是人生放棄了冒險,也就放棄了無數(shù)的可能。 ...
    yichen大刀閱讀 7,608評論 0 4

友情鏈接更多精彩內(nèi)容