相信大家在學習Go的過程中,都會看到類似這樣一句話:”與傳統的系統級線程和進程相比,協程的最大優勢在於其‘輕量級’,可以輕鬆創建上百萬個而不會導致系統資源衰竭”。那是不是意味着我們在開發過程中,可以隨心所欲的調用協程,而不關心它的數量呢?

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

這裏我先模擬一下協程數量太多的危害:

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

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

通過執行top命令查看到該程序佔用的CPU、內存較高。

爲了避免上圖這種情況,下面會簡單的介紹一下Goroutine以及在我們日常開發中如何控制Goroutine的數量。

一、基本介紹

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

爲了更好理解Goroutine,先講一下進程、線程和協程的概念。

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

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

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

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

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

func number() {
    for i := 0; i < ; i++ {
        fmt.Printf("%d ", i)
    }
}

func main() {
   go number() // 啓動一個goroutine
   number()
}

二、協程池解決?

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

顯然不需要,goroutine的初衷就是輕量級的線程,爲的就是讓你隨用隨起,結果你又搞個池子來,這不是脫褲子放屁麼?你需要的是限制併發,而協程池是一種違背了初衷的方法。池化要解決的問題一個是頻繁創建的開銷,另一個是在等待時佔用的資源。goroutine 和普通線程相比,創建和調度都不需要進入內核,也就是創建的開銷已經解決了。同時相比系統線程,內存佔用也是輕量的。所以池化技術要解決的問題goroutine 都不存在,爲什麼要創建 goroutine pool 呢?如果因爲 goroutine 持有資源而要去創建goroutine pool,那隻能說明代碼的耦合度較高,應該爲這類資源創建一個goroutine-safe的對象池,而不是把goroutine本身池化。

在我們日常大部分場景下,不需要使用協程池。因爲Goroutine非常輕量,默認2kb,使用go func()很難成爲性能瓶頸。當然一些極端情況下需要追求性能,可以使用協程池實現資源的複用,例如FastHttp使用協程池性能提高許多。

當然現在我們如果需要使用Goroutine池也不需要重複造輪子了,目前github上已經有開源的項目ants來實現 Goroutine 池。ants已經實現了對大規模 Goroutine 的調度管理、Goroutine 複用,允許使用者在開發併發程序的時候限制 Goroutine 數量,複用資源,達到更高效執行任務的效果。

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

三、 通過channel和sync方式限制協程數量

3.1 Channel

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

3.2 Sync

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

通過Golang自帶的channel和sync,可以實現需求,下面代碼中通過channel控制Goroutine數量。

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() {
      // 做一些業務邏輯處理
      fmt.Printf("go func: %d\n", value)
      time.Sleep(time.Second)
      wg.Done()
    }
    g.Run(goFunc)
  }
  wg.Wait()
}

四、總結

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

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

思考1:爲什麼我們要使用sync.WaitGroup?

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

思考2:代碼中channel數據結構爲什麼定義struct,而不定義成bool這種類型呢?

因爲空結構體變量的內存佔用大小爲0,而bool類型內存佔用大小爲1,這樣可以更加最大化利用我們服務器的內存空間。

func main(){
  a :=struct{}{}
  b := true
  fmt.Println(unsafe.Sizeof(a))  # println 0
  fmt.Println(unsafe.Sizeof(b))  # println 1
}

【作者簡介】

洋洋,攜程高級安全研發工程師,擅長Python、Golang開發,負責安全工具研發。

相關文章