SoFunction
Updated on 2025-03-05

Go sync WaitGroup usage in-depth understanding

Basic introduction

WaitGroup is a concurrency primitive used by go to do task orchestration. What it wants to solve is the concurrency-waiting problem:

When a goroutine A is waiting for a set of goroutines to complete at a checkpoint, if these goroutines have not been completed, goroutine A will block at the checkpoint until all goroutines have been completed.

Imagine if there is no WaitGroup, if you want to wait for coroutine A to execute immediately after other coroutines are executed, you can only keep polling whether other coroutines have completed execution. This problem is:

  • Poor timeliness: The higher the polling interval, the worse the timeliness
  • No need for free training, wasted system resources

When using WaitGroup, coroutine A only uses blocking until other coroutines are executed, coroutine A will be notified after coroutine A is completed.

Other languages ​​also provide similar tools, such as CountDownLatch for Java

use

Waitgroup provides 3 methods:

func (wg *WaitGroup) Add(delta int)
func (wg *WaitGroup) Done()
func (wg *WaitGroup) Wait()

Add: Increase the count value

Done: Reduce the count value

Wait: The goroutine that calls this method will block until the count value of WaitGroup becomes 0

Source code analysis

type WaitGroup struct {
   // Avoid copying   noCopy noCopy
   // In a 64-bit environment, the higher 32-bit is the count value, and the lower 32-bit record the number of waiters   state1 uint64
   // for semaphore   state2 uint32
}

Add

func (wg *WaitGroup) Add(delta int) {
   // Get the status value, semaphore   statep, semap := ()
   // Add the left 32 bits of the parameter delta to statep, that is, add delta to the count value   state := atomic.AddUint64(statep, uint64(delta)<<32)
   // Added count value   v := int32(state >> 32)
   // Number of waiters   w := uint32(state)
   // Can't be negative after addition   if v < 0 {
      panic( "sync: negative WaitGroup counter" )
   }
   // When there is a waiter, the current coroutine has added a count value, panic   // In other words, if there is a waiter, you can no longer add the count value to the waitgroup   if w != 0 && delta > 0 && v == int32(delta) {
      panic( "sync: WaitGroup misuse: Add called concurrently with Wait" )
   }
   // If v is greater than 0 after addition, or v is equal to 0 after addition, but there is no waiter, return directly   if v > 0 || w == 0 {
      return
   }
   // Next is the case where v is equal to 0 and w is greater than 0   // Check again whether there are concurrent calls between Add and Wait   if *statep != state {
      panic( "sync: WaitGroup misuse: Add called concurrently with Wait" )
   }
   // Clear count value and waiter number 0   *statep = 0
   // Wake up all waiters   for ; w != 0; w-- {
      runtime_Semrelease(semap, false, 0)
   }
}
  • Because the state has a higher 32 bits to save the count value, it is necessary to shift the parameter delta left by 32 bits and add it to the state to correct

If v is greater than 0 after addition, or v is equal to 0 after addition, but there is no waiter, return directly

  • v is greater than 0: It means that you are not the last coroutine to call Done. You don't have to release the waiter yourself, just return it directly
  • v is equal to 0, but there is no waiter: because there is no waiter, there is no need to release the waiter, and it also returns directly

Otherwise, it is the case that v is equal to 0 and w is greater than 0:

I am the last one to call Done, and there is a waiter, thenAwaken all waiting

Done

Done calls Add internally, just pass -1 to the parameter, indicating that the count value is reduced

func (wg *WaitGroup) Done() {
   (-1)
}

Wait

func (wg *WaitGroup) Wait() {
   statep, semap := ()
   for {
      state := atomic.LoadUint64(statep)
      // v: count value      v := int32(state >> 32)
      w := uint32(state)
      // If the count value is 0, you don't need to wait, just return      if v == 0 {
         return
   }
      // Increase the waiter count value if atomic.CompareAndSwapUint64(statep, state, state+1) {
         // Block yourself in the semaphore         runtime_Semacquire(semap)
         // Check whether Waitgroup is reused before wait returns         if *statep != 0 {
            panic( "sync: WaitGroup is reused before previous Wait has returned" )
         }
         return
      }
   }
}

If the count value is 0, there is no need to block at present, return directly

Otherwise, add the number of waiters by 1. If the addition is successful, block yourself on the semaphore

When awakened, if statep is not 0, it means whether the waitgroup has been reused before wait returns.

Things to note

Through source code analysis, we can see that Waitgroup has the following precautions for use:

The value of the counter must be greater than or equal to 0

When calling Add, you cannot pass negative numbers.

Done cannot be called too many times, resulting in more than the count value of WaitGroup

Therefore, the correct way to use WaitGroup is to determine the count value of WaitGroup in advance, and then call Done with the same number of times to complete the corresponding task

wantEnsure that after the expected Add call is completed, Wait will be called, otherwise Wait will not block if the count value is 0

It is best to adjust Add in a coroutine, then Wait in order.

When reusing it, you need to start a new round of use after the previous group of calls to Wait ends.

WaitGroup is reusable. As long as the count of WaitGroup returns to the zero value state, it can be regarded as a newly created WaitGroup and is reused, and cannot be used again without using the previous group.

The above is the detailed content of the in-depth understanding of the use of Go sync WaitGroup. For more information about the use of Go sync WaitGroup, please follow my other related articles!