sync.WaitGroup
, I believe all gophers have used it. It is a concurrency primitive in the package sync used for task coordination. It solves the problem of concurrency waiting: when a goroutine A is waiting at a checkpoint for a group of goroutines to complete. Without this synchronization primitive, how can we achieve this functionality? There are many ways; let’s first try using channels to implement it.
This article was first published in the Medium MPP plan. If you are a Medium user, please follow me on Medium. Thank you very much.
|
|
Although this code can fulfill the functionality, it is cumbersome. We not only need to create a channel but also create a goroutine to wait for this channel. Moreover, if the number of goroutines we need to wait for is uncertain, this approach is not very suitable. Therefore, sync.WaitGroup comes into play.
Now, let’s implement the above functionality in a different way using sync.WaitGroup link.
|
|
In fact, many operating systems and programming languages provide similar concurrency primitives. For example, the barrier in Linux, the barrier in Pthread (POSIX threads), std::barrier in C++, CyclicBarrier and CountDownLatch in Java, and so on. This concurrency primitive is therefore a very fundamental type of concurrency.
Basic Usage of WaitGroup
The basic usage of WaitGroup is very simple, with only three methods:
|
|
The Add method is used to increase the number of goroutines to wait for, the Done method is used to decrease the number of goroutines to wait for, and the Wait method is used to wait for all goroutines to complete.
Implementation of WaitGroup
This article is based on go1.21.4
First, let’s take a look at the data structure of WaitGroup:
|
|
The data structure of WaitGroup is very simple, with only two fields: state and sema. Among them, state is an atomic.Uint64 type used to store the counter and the number of waiting goroutines. sema is a semaphore used for waiting.
Add Method
Let’s take a look at the implementation of the Add method, removing the race detection.
|
|
The implementation of the Add method is very simple. It left-shifts the delta by 32 bits and adds it to the state. Then, it checks if the counter is less than 0, and if so, it panics. It also checks if the waiter count is not 0 and if the delta is greater than 0 and the counter is equal to delta, it panics. If the counter is greater than 0 or the waiter count is 0, it returns. If the state has been modified by other goroutines, it panics. Finally, it sets the waiter count to 0 and wakes up all waiters.
Done Method
The implementation of the Done method is very simple. It calls the Add method with a parameter of -1.
Wait Method
The logic of the Wait method is as follows: it continuously checks the value of the state. If the counter becomes 0, it means that all tasks have been completed, and the caller does not need to wait any longer, so it returns directly. If the counter is greater than 0, it means that there are still tasks that have not been completed, so the caller becomes a waiter, needs to join the waiter queue, and blocks itself.
|
|
Common Errors when Using WaitGroup
- The high 32 bits of the state are the counter, and the low 32 bits are the waiter count. If the value of the counter exceeds 2^31-1, it will panic. This problem rarely occurs in practical use because this value is too large.
- Add and Done do not appear in pairs, which means that if you add n times, you should also call Done n times. If you add n times but call Done m times, where m < n, it will panic or cause deadlock.
- Unexpected timing of Add. If Add is called after Wait, it will panic. Because this will cause the counter to become 0, but there are still goroutines waiting.
- Reusing WaitGroup.
Bugs in WaitGroup usage in real projects
- In Golang issue 28123, the biggest problem in this code is that line 9 copies the WaitGroup instance w. Although this code can be executed successfully, it does violate the rule of not copying the WaitGroup instance after use. In projects, we can use the vet tool to detect such errors.
- Docker issue 28161 and issue 27011 are both errors caused by reusing WaitGroup without waiting for the previous Wait to finish before calling Add.
- Etcd issue 6534 is also a bug in reusing WaitGroup, where Add is called without waiting for the previous Wait to finish.
- Kubernetes issue 59574 is a bug in which the Wait is forgotten before increasing the count. This bug is considered almost impossible to occur in our usual understanding.
Conclusion
sync.WaitGroup is a very basic concurrency primitive. Its implementation is very simple, but it has many pitfalls. When using it, you must pay attention to not copying the WaitGroup instance, not reusing the WaitGroup instance, not calling Add after Wait, and ensuring that Add and Done appear in pairs. In real projects, we can use the go vet
tool to check for these issues.