相信大家在學習Go的過程中谓松,都會看到類似這樣一句話:"與傳統(tǒng)的系統(tǒng)級線程和進程相比,協(xié)程的最大優(yōu)勢在于其‘輕量級’,可以輕松創(chuàng)建上百萬個而不會導致系統(tǒng)資源衰竭"咐汞。那是不是意味著我們在開發(fā)過程中概行,可以隨心所欲的調用協(xié)程蠢挡,而不關心它的數量呢?
答案當然是否定的凳忙。我們在開發(fā)過程中业踏,如果不對Goroutine加以控制而進行濫用的話,可能會導致服務程序整體崩潰涧卵。
這里我先模擬一下協(xié)程數量太多的危害:
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是用戶輸入的一個參數勤家,沒有做限制。有些開發(fā)人員會全部丟進去進行循環(huán)柳恐,認為全部都并發(fā)使用Goroutine去做一件事情伐脖,效率比較高。但這樣的話乐设,噩夢般的事情就開始了讼庇,服務器系統(tǒng)資源利用率不斷上漲,到最后程序自動killed伤提。
通過執(zhí)行top命令查看到該程序占用的CPU巫俺、內存較高。
為了避免上圖這種情況肿男,下面會簡單的介紹一下Goroutine以及在我們日常開發(fā)中如何控制Goroutine的數量介汹。
一、基本介紹
工欲善其事必先利其器舶沛。先簡單的介紹一下Goroutine嘹承,Goroutine是Go中最基本的執(zhí)行單元。事實上每一個Go程序至少有一個Goroutine:主Goroutine如庭。當程序啟動時叹卷,它會自動創(chuàng)建撼港。
為了更好理解Goroutine,先講一下進程骤竹、線程和協(xié)程的概念帝牡。
進程(process):用戶下達運行程序的命令后,就會產生進程蒙揣。同一程序可產生多個進程(一對多關系)靶溜,以允許同時有多位用戶運行同一程序,卻不會相沖突懒震。進程需要一些資源才能完成工作罩息,如CPU使用時間、存儲器个扰、文件以及I/O設備瓷炮,且為依序逐一進行,也就是每個CPU核心任何時間內僅能運行一項進程递宅。進程的局限是創(chuàng)建娘香、撤銷和切換的開銷比較大。
線程(Thread):有時被稱為輕量級進程(Lightweight Process恐锣,LWP)茅主,是程序執(zhí)行流的最小單元。一個標準的線程由線程ID土榴,當前指令指針(PC)诀姚,寄存器集合和堆棧組成。另外玷禽,線程是進程中的一個實體赫段,是被系統(tǒng)獨立調度和分派的基本單位,線程自己不擁有系統(tǒng)資源矢赁,只擁有一點兒在運行中必不可少的資源糯笙,但它可與同屬一個進程的其它線程共享進程所擁有的全部資源。線程擁有自己獨立的棧和共享的堆撩银,共享堆给涕,不共享棧,線程的切換一般也由操作系統(tǒng)調度额获。
協(xié)程(coroutine):又稱微線程與子例程(或者稱為函數)一樣够庙,協(xié)程(coroutine)也是一種程序組件。相對子例程而言抄邀,協(xié)程更為一般和靈活耘眨,但在實踐中使用沒有子例程那樣廣泛。和線程類似境肾,共享堆剔难,不共享棧胆屿,協(xié)程的切換一般由程序員在代碼中顯式控制。它避免了上下文切換的額外耗費偶宫,兼顧了多線程的優(yōu)點非迹,簡化了高并發(fā)程序的復雜。
Goroutine和其他語言的協(xié)程(coroutine)在使用方式上類似纯趋,但從字面意義上來看不同(一個是Goroutine彻秆,一個是coroutine),再就是協(xié)程是一種協(xié)作任務控制機制结闸,在最簡單的意義上,協(xié)程不是并發(fā)的酒朵,而Goroutine支持并發(fā)的桦锄。因此Goroutine可以理解為一種Go語言的協(xié)程,同時它可以運行在一個或多個線程上蔫耽。
在Go中生成一個Goroutine的方式非常的簡單:只要在函數前面加上go就生成了结耀。
func number() {
for i := 0; i < ; i++ {
fmt.Printf("%d ", i)
}
}
func main() {
go number() // 啟動一個goroutine
number()
}
二、協(xié)程池解決匙铡?
回到開頭的問題图甜,如何控制Goroutine的數量?相信有過開發(fā)經驗的人鳖眼,第一想法是生成協(xié)程池黑毅,通過協(xié)程池控制連接的數量,這樣每次連接都從協(xié)程池里去拿钦讳。在Golang開發(fā)中需要協(xié)程池嗎矿瘦?這里分享下知乎有個相關點贊最高的回答:
顯然不需要,goroutine的初衷就是輕量級的線程愿卒,為的就是讓你隨用隨起缚去,結果你又搞個池子來,這不是脫褲子放屁么琼开?你需要的是限制并發(fā)易结,而協(xié)程池是一種違背了初衷的方法。池化要解決的問題一個是頻繁創(chuàng)建的開銷柜候,另一個是在等待時占用的資源搞动。goroutine 和普通線程相比,創(chuàng)建和調度都不需要進入內核改橘,也就是創(chuàng)建的開銷已經解決了滋尉。同時相比系統(tǒng)線程,內存占用也是輕量的飞主。所以池化技術要解決的問題goroutine 都不存在狮惜,為什么要創(chuàng)建 goroutine pool 呢高诺?如果因為 goroutine 持有資源而要去創(chuàng)建goroutine pool,那只能說明代碼的耦合度較高碾篡,應該為這類資源創(chuàng)建一個goroutine-safe的對象池虱而,而不是把goroutine本身池化。
在我們日常大部分場景下开泽,不需要使用協(xié)程池牡拇。因為Goroutine非常輕量,默認2kb穆律,使用go func()很難成為性能瓶頸惠呼。當然一些極端情況下需要追求性能,可以使用協(xié)程池實現(xiàn)資源的復用峦耘,例如FastHttp使用協(xié)程池性能提高許多剔蹋。
當然現(xiàn)在我們如果需要使用Goroutine池也不需要重復造輪子了,目前github上已經有開源的項目ants來實現(xiàn) Goroutine 池辅髓。ants已經實現(xiàn)了對大規(guī)模 Goroutine 的調度管理泣崩、Goroutine 復用,允許使用者在開發(fā)并發(fā)程序的時候限制 Goroutine 數量洛口,復用資源矫付,達到更高效執(zhí)行任務的效果。
三第焰、 通過channel和sync方式限制協(xié)程數量
3.1 Channel
Goroutine運行在相同的地址空間买优,因此訪問共享內存必須做好同步。那么Goroutine之間如何進行數據的通信呢樟遣?Go提供了一個很好的通信機制channel而叼,channel可以與 Unix shell 中的雙向管道做類比:可以通過它發(fā)送或者接收值。這些值只能是特定的類型:channel類型豹悬。定義一個channel時葵陵,也需要定義發(fā)送到channel的值的類型。注意瞻佛,必須使用make創(chuàng)建channel脱篙。
3.2 Sync
Go語言中有一個sync.WaitGroup,WaitGroup 對象內部有一個計數器伤柄,最初從0開始绊困,它有三個方法:Add(), Done(), Wait() 用來控制計數器的數量。下面示例代碼中wg.Wati會阻塞代碼的運行适刀,直到計數器值為0秤朗。
通過Golang自帶的channel和sync,可以實現(xiàn)需求笔喉,下面代碼中通過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() {
// 做一些業(yè)務邏輯處理
fmt.Printf("go func: %d\n", value)
time.Sleep(time.Second)
wg.Done()
}
g.Run(goFunc)
}
wg.Wait()
}
四硝皂、總結
在文章的開頭通過在服務器模擬Goroutine數量太多導致系統(tǒng)資源上升,提醒大家避免這類問題作谭。當然每個人可根據自己所在的場景選擇最合適的方案稽物,有時候成熟的第三方庫也是個很好的選擇,可以避免重復造輪子折欠。
下面有兩個思考問題贝或,大家可以嘗試著去思考一下。
思考1:為什么我們要使用sync.WaitGroup锐秦?
這里如果我們不使用sync.WaitGroup控制的話咪奖,原因出在當主程序結束時,子協(xié)程也是會被終止掉的酱床。因此剩余的 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
}