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

在使用Golang一段時(shí)間之后,很少人不認(rèn)識(shí)一個(gè)叫sync.WaitGroup的結(jié)構(gòu)體。

使用場(chǎng)景

WaitGroup用來(lái)編排多個(gè)并發(fā)任務(wù),舉個(gè)例子,一個(gè)業(yè)務(wù)邏輯,依賴(lài)三個(gè)HTTP請(qǐng)求,且這三個(gè)HTTP請(qǐng)求可以并發(fā)執(zhí)行,之間并無(wú)依賴(lài)關(guān)系,這就是WaitGroup使用的場(chǎng)景。

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() // 到這里的時(shí)候,所有的請(qǐng)求都執(zhí)行完畢

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

信號(hào)量

在剖析WaitGroup之前,我們必須先說(shuō)下信號(hào)量,給后面的內(nèi)容打下堅(jiān)實(shí)的基礎(chǔ)。

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

如果你了解一點(diǎn)Java的話,下面的例子就是最好的闡述,如果不懂也沒(méi)有關(guān)系,這幾行代碼你一定能看懂:

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

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

需要注意的是,在Java中,new Runnable有點(diǎn)像golang里的go func,是跑在一個(gè)單獨(dú)的線程中的,許可,或者說(shuō)資源是有限的,多個(gè)線程爭(zhēng)搶?zhuān)捅仨氂幸粋€(gè)機(jī)制能編排它們,這就是信號(hào)量的作用。

讓我們來(lái)歸納一下。

信號(hào)量就是一個(gè)有限許可集,信號(hào)量的使用場(chǎng)景有以下特點(diǎn):

  1. 資源有限。

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

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

數(shù)據(jù)結(jié)構(gòu)和算法

WaitGroup的結(jié)構(gòu)體是這樣的:

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

noCopy只是標(biāo)記下WaitGroup是不可以被拷貝的,state1中分為一個(gè)64位和一個(gè)32位,64位用來(lái)計(jì)數(shù)worker和waiter,另外的32位用來(lái)作信號(hào)量的底層數(shù)據(jù)結(jié)構(gòu)。

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

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

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

  1. 修改worker的值,這里只所以說(shuō)修改,而不說(shuō)遞增,是因?yàn)锳dd的參數(shù)可能是正的,也可能是負(fù)的。

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

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

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

  1. waiter的值遞增。

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

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

內(nèi)存對(duì)齊

關(guān)于內(nèi)存對(duì)齊的文章很多,這里并不打算宣兵奪主,還是回到我們的WaitGroup上來(lái)。之所以提內(nèi)存對(duì)齊,是因?yàn)榭紤]了內(nèi)存對(duì)齊,所以結(jié)構(gòu)體里的state1是一個(gè)長(zhǎng)度為3的數(shù)組才有一個(gè)合理的解釋。

內(nèi)存對(duì)齊的知識(shí)點(diǎn)可能很多,但我們今天只關(guān)心其中一個(gè)。

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.

也就是說(shuō)32位架構(gòu)想要原子性的操作8bytes,需要由調(diào)用方保證其數(shù)據(jù)地址是64位對(duì)齊的,否則原子訪問(wèn)會(huì)有異常。

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

參考

Java semaphore:

https://segmentfault.com/a/1190000023038654

WaitGroup:

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

內(nèi)存對(duì)齊:

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

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

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容