Golang利用sync.WaitGroup實現協程同步詳解

協程同步

在實際項目開發過程中經常會遇到併發需要協程同步的場景,經常看到有人會問如何等待主協程中創建的協程執行完畢之後再結束主協程,例如下面代碼,通過起100個協程實現併發打印的例子:

package main

import (
    "fmt"
)

func main() {
    for i := 0; i < 100 ; i++{
		go func(i int) {
			fmt.Println("Goroutine ",i)
		}(i)
	}
}

執行以上代碼很可能看不到輸出,也可能只執行了部分協程,因爲有可能這100個協程還沒得到執行主協程已經結束了,或者執行了部分協程主協程執行完了,而主協程結束時會結束所有其他協程。解決辦法是可以在main函數結尾加上sleep()等待:

package main

import (
    "fmt"
    "time"
)

func main() {
  for i := 0; i < 100 ; i++{
		go func(i int) {
			fmt.Println("Goroutine ",i)
		}(i)
	}
    time.Sleep(time.Second * 1) // 睡眠1秒,等待上面兩個協程結束
}

主線程爲了等待goroutine都運行完畢,不得不在程序的末尾使用time.Sleep() 來睡眠一段時間,等待其他線程充分運行。對於簡單的代碼,多個for循環可以在1秒之內運行完畢,time.Sleep() 也可以達到想要的效果。
但是對於實際生活的大多數場景來說,1秒是不夠的,並且大部分時候我們都無法預知for循環內代碼運行時間的長短。這時候就不能使用time.Sleep() 來完成等待操作了。這並不是完美的解決方法,如果這兩個協程中包含複雜的操作,可能很耗時間,就無法確定需要睡眠多久,當然可以用管道實現同步:

package main

import (
    "fmt"
)

func main() {

    ch := make(chan struct{})
    count := 100 // count 表示活動的協程個數
    
   for i := 0; i < 100 ; i++{
		go func(i int) {
			fmt.Println("Goroutine ",i)
                        ch <- struct{}{} // 協程結束,發出信號
		}(i)
	}
   
    for range ch {
        // 每次從ch中接收數據,表明一個活動的協程結束
        count--
        // 當所有活動的協程都結束時,關閉管道
        if count == 0 {
            close(ch)
        }
    }
}

上面的解決方案是比較完美的方案,首先可以肯定的是使用管道是能達到我們的目的的,而且不但能達到目的,還能十分完美的達到目的。但是管道在這裏顯得有些大材小用,因爲它被設計出來不僅僅只是在這裏用作簡單的同步處理,在這裏使用管道實際上是不合適的。而且假設我們有一萬、十萬甚至更多的for循環,也要申請同樣數量大小的管道出來,對內存也是不小的開銷。Go提供了更簡單的方法——使用sync.WaitGroup。WaitGroup顧名思義,就是用來等待一組操作完成的。WaitGroup內部實現了一個計數器,用來記錄未完成的操作個數,它提供了三個方法,Add()用來添加計數。Done()用來在操作結束時調用,使計數減一。Wait()用來等待所有的操作結束,即計數變爲0,該函數會在計數不爲0時等待,在計數爲0時立即返回。

package main

import (
    "fmt"
    "sync"
)

func main() {

    var wg sync.WaitGroup

    wg.Add(100) // 因爲有兩個動作,所以增加2個計數
     for i := 0; i < 100 ; i++{
		go func(i int) {
			fmt.Println("Goroutine ",i)
                       wg.Done() // 操作完成,減少一個計數
		}(i)
	}

    wg.Wait() // 等待,直到計數爲0
}

可見用sync.WaitGroup是最簡單的方式。

注意事項

1. 計數器不能爲負值
我們不能使用Add() 給wg 設置一個負值,否則代碼將會報錯:

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

同樣使用Done() 也要特別注意不要把計數器設置成負數了。

2. WaitGroup對象不是一個引用類型
WaitGroup對象不是一個引用類型,在通過函數傳值的時候需要使用地址:

func main() {
    wg := sync.WaitGroup{}
    wg.Add(100)
    for i := 0; i < 100; i++ {
        go f(i, &wg)
    }
    wg.Wait()
}
 
// 一定要通過指針傳值,不然進程會進入死鎖狀態
func f(i int, wg *sync.WaitGroup) { 
    fmt.Println(i)
    wg.Done()
}

相互學習。共同進步。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章