Golang中的sync包的WaitGroup操作

sync的waitgroup功能

WaitGroup

使用多線程時,進行等待多線程執行完畢後,才可以結束函數,有兩個選擇

channel

waitgroup

首先使用channel

func add (n *int , isok chan bool){
	for i :=0 ;i <1000 ; i ++ {
		*n = *n + 1
	}
	isok <- true
}
func main () {
	var ok = make(chan bool , 2)
	var i,u = 0,0
	go add(&i , ok)
	go add(&i , ok)
	for <- ok {
		u++
		if u == 2 {
			break
		}
	}
	fmt.Println(i)
}

但是,如果線程一旦增多,就會導致代碼冗餘,增加負擔,可以使用sync.WaitGroup包

func add (n *int , wait *sync.WaitGroup) {
 for i := 0 ; i <1000 ; i ++ {
  *n = *n + 1
 }
 defer wait.Done()
}
func main () {
  var wait sync.WaitGroup
 var i = 0
 wait.Add(2)
  go add(&i , &wait)
 go add(&i , &wait)
 wait.Wait()
 fmt.Println(i)
 }

仿sync.WaitGroup功能

type Wait struct {//創建一個結構體
 Num int//線程的個數
 ok chan bool//線程與函數通信的管道
}
func (t *Wait) Add (n int){//初始化線程個數
 t.Num = n
 t.ok = make(chan bool , n)
}
func (t *Wait) Done (){//執行完一個線程之後,執行的函數,每執行完一個線程,調用函數,使用通信管道進行傳輸一個true
 t.ok <- true
}
func (t *Wait) Wait () {//等待函數,每次管道中放入一個true,說明,執行完一個線程,t.Num--,如果等於0說明所有線程執行結束
 for <- t.ok {
  t.Num--
  if t.Num == 0 {
   break
  }
 }
}

補充:Golang的WaitGroup陷阱

sync.WaitGroup是並發環境中,一個相當常用的數據結構,用來等待所有協程的結束,在寫代碼的時候都是按著例子的樣子寫的,也沒用深究過它的使用。前幾日想著能不能在協程中執行Add()函數,答案是不能,這裡介紹下。

陷阱在WaitGroup的3個函數的調用順序上。先回顧下3個函數的功能:

Add(delta int):給計數器增加delta,比如啟動1個協程就增加1。

Done():協程退出前執行,把計數器減1。

Wait():阻塞等待計數器為0。

考一考

下面的程序是創建瞭協程father,然後father協程創建瞭10個子協程,main函數等待所有協程結束後退出,看看下面代碼有沒有什麼問題?

package main 
import (
    "fmt"
    "sync"
)
 
func father(wg *sync.WaitGroup) {
    wg.Add(1)
    defer wg.Done()
 
    fmt.Printf("father\n")
    for i := 0; i < 10; i++ {
        go child(wg, i)
    }
}
 
func child(wg *sync.WaitGroup, id int) {
    wg.Add(1)
    defer wg.Done()
 
    fmt.Printf("child [%d]\n", id)
}
 
func main() {
    var wg sync.WaitGroup
    go father(&wg)
 
    wg.Wait()
    log.Printf("main: father and all chindren exit")
}

發現問題瞭嗎?如果沒有看下面的運行結果:main函數在子協程結束前就開始結束瞭。

father
main: father and all chindren exit
child [9]
child [0]
child [4]
child [7]
child [8]

陷阱分析

產生以上問題的原因在於,創建協程後在協程內才執行Add()函數,而此時Wait()函數可能已經在執行,甚至Wait()函數在所有Add()執行前就執行瞭,Wait()執行時立馬就滿足瞭WaitGroup的計數器為0,Wait結束,主程序退出,導致所有子協程還沒完全退出,main函數就結束瞭。

正確的做法

Add函數一定要在Wait函數執行前執行,這在Add函數的文檔中就提示瞭: Note that calls with a positive delta that occur when the counter is zero must happen before a Wait.。

如何確保Add函數一定在Wait函數前執行呢?在協程情況下,我們不能預知協程中代碼執行的時間是否早於Wait函數的執行時間,但是,我們可以在分配協程前就執行Add函數,然後再執行Wait函數,以此確保。

下面是修改後的程序,以及輸出結果。

package main 
import (
    "fmt"
    "sync"
)
 
func father(wg *sync.WaitGroup) {
    defer wg.Done()
 
    fmt.Printf("father\n")
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go child(wg, i)
    }
}
 
func child(wg *sync.WaitGroup, id int) {
    defer wg.Done()
    fmt.Printf("child [%d]\n", id)
}
 
func main() {
    var wg sync.WaitGroup
    wg.Add(1)
    go father(&wg)
 
    wg.Wait()
    fmt.Println("main: father and all chindren exit")
}
father
child [9]
child [7]
child [8]
child [1]
child [4]
child [5]
child [2]
child [6]
child [0]
child [3]
main: father and all chindren exit

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

推薦閱讀: