解決Golang 中使用WaitGroup的那點坑

sync.WaitGroup對於Golang開發者來說並不陌生,其經常作為多協程之間同步的一種機制。用好它勢必會讓你事半功倍,但是一旦錯用將引發問題。

關於WaitGroup的使用網上有很多例子,在此就不做介紹瞭,我想說的是我在項目中使用WaitGroup遇到的坑。

在項目中,因為服務器有同步需求, 所以直接使用瞭WaitGroup,但是未考慮使用場景,結果在項目上線之後,高峰期的時候客戶端經常出現卡頓,經過多方查找,才發現如果使用WaitGroup的時候,未啟動單獨的goroutine,那麼極有可能造成主線程的阻塞

所以我做瞭下面的測試(測試中,我把WaitGroup置於協程內):

import (
 "fmt"
 "sync"
 "time"
)
 
func main() {
    fmt.Println("main-1")
 testW()
 fmt.Println("main-2")
 time.Sleep(time.Duration(15) * time.Second) 
}
 
func testW() {
 fmt.Println("testW-1")
 go func() {
  var wg sync.WaitGroup
  fmt.Println("testW-2")
  testW1(&wg)
  fmt.Println("testW-5")
  wg.Wait()
  fmt.Println("testW-6")
 }()
}
 
func testW1(wg *sync.WaitGroup) {
 wg.Add(1)
 fmt.Println("testW-3")
 time.AfterFunc(time.Second*5, func() {
  wg.Done()
 })
 fmt.Println("testW-4")
 
}

輸出為:

main-1

testchan-1

main-2

testchan-2

testchan-3

testchan-4

testchan-5

// 過5秒

testchan-6

總結:

將WaitGroup用於goroutine內,不會導致主線程的阻塞,同樣可以實現同步的效果。

補充:WaitGroup的簡單用法(等待組)

你品一下人傢這名字,等待組。等待什麼,等待goroutine完成啊。有些時候,我們啟動多個goroutine去執行任務,我舉個例子

listip := []string{"10.0.9.11","10.0.9.22","10.0.9.33"}
for _, ip := range(listip) {
    //假設我們執行一個ping ip 的邏輯
    go PingIPWork(ip)
}

我這裡執行瞭一個多ip去ping的邏輯,一般這種時候,你要是執行一波,人傢肯定毛都不會返回給你,為什麼?因為人傢主線程直接就退出瞭,還是那句話,你又沒告訴人傢主線程要等這ip全部都ping 完,所以你必須要加個等待,等著Goroutine完成,這裡我再舉一個網上的例子

package main
import (
    "fmt"
)
func main() {
    go func() {
        fmt.Println("Goroutine 1")
    }()
    go func() {
        fmt.Println("Goroutine 2")
    }()
    //來個睡眠,等Goroutine結束
    time.Sleep(time.Second * 1)
}

看到瞭麼,加瞭一個sleep,用sleep去等著Goroutine跑完,上面我舉的那個例子也可以這麼來

listip := []string{"10.0.9.11","10.0.9.22","10.0.9.33"}
for _, ip := range(listip) {
    //假設我們執行一個ping ip 的邏輯
    go PingIPWork(ip)
}
time.Sleep(time.Second * 1)

加個sleep可以等待完成,但是萬一啊,Goroutine有的跑的快,有的慢,你那sleep就一秒,要是有的Goroutine沒跑完不就白瞎瞭嗎,所以咱們需要一個機制,這個機制可以幫助咱們去管理Goroutine,讓我們知道Goroutine這東西什麼時候停,什麼時候完成。

所以,WaitGroup這個東西,就可以幫助我們解決這個問題,還是老樣子,我舉一個簡單的例子來說明我的想法。

package main
import (
    "fmt"
 "sync"
)
func PingIPWork(ip string) {
 fmt.Println(ip)
}
func main() {
    //定義一個等待阿祖
 var wg sync.WaitGroup
 wg.Add(3) // 因為有3個Ip,咱們定義三個動作,所以來三個計數
 listip := []string{"10.0.9.11","10.0.9.22","10.0.9.33"}
 for _, ip := range(listip) {
  //假設我們執行一個ping ip 的邏輯
  go func(ip string) {
            //執行一個work
            PingIPWork(ip)
            //操作完成之後,done一個計數,也就是3-1
   wg.Done()
  }(ip)
 }
    //等待
 wg.Wait() // 等待,直到計數為0
}

這裡我舉瞭一個簡單的例子,其實wg的用法較為簡單,在這個例子裡面我們用到瞭

wg.wait
等待Goroutine結束之後退出主進程
wg.Add
添加Goroutine,其實你可以把它想成,可添加的最大Goroutine數
wg.Done
想象成銷毀參數,當Goroutine結束之後調用,意思就是,你沒瞭,我減1

WaitGroup的其他註意事項

將Wg作為參數進行傳遞的時候,需要使用指針

有些時候,咱們不想寫的這麼麻煩,就尋思怎麼才能簡單一點,或者可變性稍微強一點,有些時候我們要把wg最為參數,在函數內部調用,我們該怎麼寫呢?

package main
import (
 "fmt"
 "sync"
)
func PingIPWork(ip string, wg *sync.WaitGroup) {
 fmt.Println(ip)
 wg.Done()
}
func main() {
 var wg sync.WaitGroup
 wg.Add(3) // 因為有兩個動作,所以增加2個計數
 listip := []string{"10.0.9.11","10.0.9.22","10.0.9.33"}
 for _, ip := range(listip) {
  //假設我們執行一個ping ip 的邏輯
  go PingIPWork(ip, &wg)
  }
 wg.Wait() // 等待,直到計數為0
}

看到瞭麼,如果你把Wg作為參數進行傳遞,你得要用指針的形式傳值,否則就會死鎖!!!!!!!!

Wg.Add的數值不能為負

wg.Add()的數值必須為正數,如果為負數,將會拋出異常。

panic: sync: negative WaitGroup counter
goroutine 1 [running]:
sync.(*WaitGroup).Add(0xc042008230, 0xffffffffffffff9c)
    D:/Go/src/sync/waitgroup.go:75 +0x1d0
main.main()
    D:/code/go/src/test-src/2-Package/sync/waitgroup/main.go:10 +0x54

以上為個人經驗,希望能給大傢一個參考,也希望大傢多多支持WalkonNet。如有錯誤或未考慮完全的地方,望不吝賜教。

推薦閱讀: