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

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

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

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

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

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

image.png

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

image.png

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

一、基本介紹

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

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

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

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

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

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

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

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

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

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

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

在我們?nèi)粘4蟛糠謭鼍跋拢恍枰褂脜f(xié)程池。因?yàn)镚oroutine非常輕量,默認(rèn)2kb,使用go func()很難成為性能瓶頸。當(dāng)然一些極端情況下需要追求性能,可以使用協(xié)程池實(shí)現(xiàn)資源的復(fù)用,例如FastHttp使用協(xié)程池性能提高許多。

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

項(xiàng)目地址:https://github.com/panjf2000/ants

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

3.1 Channel

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

3.2 Sync

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

通過Golang自帶的channel和sync,可以實(shí)現(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è)務(wù)邏輯處理
      fmt.Printf("go func: %d\n", value)
      time.Sleep(time.Second)
      wg.Done()
    }
    g.Run(goFunc)
  }
  wg.Wait()
}

四、總結(jié)

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

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

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

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

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

因?yàn)榭战Y(jié)構(gòu)體變量的內(nèi)存占用大小為0,而bool類型內(nèi)存占用大小為1,這樣可以更加最大化利用我們服務(wù)器的內(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)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容

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