WaitGroup是如何實現(xiàn)的?

在使用Golang一段時間之后,很少人不認識一個叫sync.WaitGroup的結構體。

使用場景

WaitGroup用來編排多個并發(fā)任務,舉個例子,一個業(yè)務邏輯,依賴三個HTTP請求,且這三個HTTP請求可以并發(fā)執(zhí)行,之間并無依賴關系,這就是WaitGroup使用的場景。

var urls = []string{"url1", "url2", "url3"}
g := sync.WaitGroup{}
g.Add(len(urls))
for i := 0; i < len(urls); i++ {
  go func(url string) {
    defer g.Done() // or g.Add(-1)
    request(url)
  }(urls[i])
}
g.Wait() // 到這里的時候,所有的請求都執(zhí)行完畢

使用WaitGroup,能輕易的編排并發(fā),使得主業(yè)務邏輯等待的時間等于三個請求中最慢的時間。

信號量

在剖析WaitGroup之前,我們必須先說下信號量,給后面的內容打下堅實的基礎。

信號量(semaphore)是一個許可集。至少,這是許多文章里都提到的一個概念,許可集其實是許可集合,既然是集合,那就是有限的,也就是說,多個線程或者協(xié)程競爭許可集合里的許可,如果有富余許可,就給等待的線程,線程拿到許可后,用完放回,沒有拿到線程,則休眠等待喚醒,重新競爭。

如果你了解一點Java的話,下面的例子就是最好的闡述,如果不懂也沒有關系,這幾行代碼你一定能看懂:

Semaphore s = new Semaphore(3); // 許可個數(shù)
Runnable run = new Runnable() {
 public void run() {
   try {
     s.acquire(); // 拿不到則阻塞
   } catch (InterruptedException e) {

   } finally {
     s.release(); // 用完放回
   }
 }
}

需要注意的是,在Java中,new Runnable有點像golang里的go func,是跑在一個單獨的線程中的,許可,或者說資源是有限的,多個線程爭搶,就必須有一個機制能編排它們,這就是信號量的作用。

讓我們來歸納一下。

信號量就是一個有限許可集,信號量的使用場景有以下特點:

  1. 資源有限。

  2. 并發(fā)(多線程)。

在Golang中,信號量也是實現(xiàn)鎖所依賴的基本函數(shù)。在其他場景下,比如說連接池,也是使用信號量非常合適的業(yè)務場景。

數(shù)據(jù)結構和算法

WaitGroup的結構體是這樣的:

type WaitGroup struct {
  noCopy noCopy
  state1 [3]uint32
}

noCopy只是標記下WaitGroup是不可以被拷貝的,state1中分為一個64位和一個32位,64位用來計數(shù)worker和waiter,另外的32位用來作信號量的底層數(shù)據(jù)結構。

通常情況下,使用WaitGroup都是在主協(xié)程中執(zhí)行Add和Wait,在并發(fā)的協(xié)程中執(zhí)行Done,Wait方法可以是出現(xiàn)在多個協(xié)程中,被重復調用,但通常出現(xiàn)在主協(xié)程中。

worker的數(shù)量可以認為是在執(zhí)行中的協(xié)程的數(shù)量,waiter的數(shù)量可以認為是有多少個協(xié)程調用了Wait方法在等待所有的worker執(zhí)行完畢。

Add函數(shù)的主要邏輯有2個:

  1. 修改worker的值,這里只所以說修改,而不說遞增,是因為Add的參數(shù)可能是正的,也可能是負的。

  2. 如果worker的值變?yōu)榱?,說明所有的協(xié)程都執(zhí)行完畢,就要釋放許可,釋放的數(shù)量,就是waiter的數(shù)量。

Done函數(shù)的邏輯其實是調用了Add(-1),所以Done的邏輯參考Add的邏輯。

Wait函數(shù)的主要邏輯也是2個:

  1. waiter的值遞增。

  2. 等待信號量,阻塞獲取許可,獲取到以后就返回,函數(shù)結束。

歸納WaitGroup的實現(xiàn),本質上是圍繞信號量實現(xiàn)的,但是什么時候釋放,釋放幾個,什么時候獲取信號量,這些都是用state1這個field來實現(xiàn)的。算法的實現(xiàn)總是需要用數(shù)據(jù)結構作為依托。

內存對齊

關于內存對齊的文章很多,這里并不打算宣兵奪主,還是回到我們的WaitGroup上來。之所以提內存對齊,是因為考慮了內存對齊,所以結構體里的state1是一個長度為3的數(shù)組才有一個合理的解釋。

內存對齊的知識點可能很多,但我們今天只關心其中一個。

On ARM, x86-32, and 32-bit MIPS, it is the caller's responsibility to arrange for 64-bit alignment of 64-bit words accessed atomically. The first word in a variable or in an allocated struct, array, or slice can be relied upon to be 64-bit aligned.

也就是說32位架構想要原子性的操作8bytes,需要由調用方保證其數(shù)據(jù)地址是64位對齊的,否則原子訪問會有異常。

在閱讀WaitGroup源碼的時候,會注意到,state1在64位架構下,前64位是worker和waiter計數(shù)器,后32位是sema,而在32位架構下,前32位是sema,后64位是worker和waiter計數(shù)器。不得不感嘆思路之精巧、精密,計算機基礎知識之深,我們還有很長的路要走。

參考

Java semaphore:

https://segmentfault.com/a/1190000023038654

WaitGroup:

https://www.ququ123.xyz/2022/04/golang_wait_group_principle/

內存對齊:

https://www.cnblogs.com/luozhiyun/p/14289034.html

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容