Welcome back to the series tutorials on Go Concurrency. If you missed the previous tutorials please refer to: Go Concurrency with GoRoutines.
In this tutorial, we will help you to understand the concept of WaitGroup.
In some scenarios, you may need to block certain parts of code to allow these GoRoutines to complete their execution according to your needs. A common usage of WaitGroup is to block the main function because we know that the main function itself is also a GoRoutine.
package main import ( "fmt" "sync" ) func main() { wg := sync.WaitGroup{} wg.Add(1) go print(&wg) wg.Wait() fmt.Println("hello from main") } func print(wg *sync.WaitGroup) { fmt.Println("hello from goroutine") wg.Done() }
You need to import the sync package before you can use WaitGroup. WaitGroup is of struct type and it has three functions, which you can use: Add(int), Wait() and Done(). The mechanics of the WaitGroup is that it has a counter that starts at zero. Whenever you call Add(int), you are increasing the counter by the parameter specified in the Add(int) function.
On the other hand, each Done() function decreases the counter by 1. The WaitGroup panics if the counter is less than 0 and therefore, you need to make sure Done() is not called more than you need to. Moreover, if you use Done() before Add(), it will also cause a panic. The Wait() function blocks the code and it will be released until the counter is zero.
In the example, we first declare a variable wg and instantiate a new sync.WaitGroup{}. We call Add(1) before attempting to execute our go print(). We then pass the pointer to wg in print() so that we can use Done() function once the print task is completed. We call wg.Wait() to block the main GoRoutine and it will release the block until the go print() is done.
A little exercise for the reader, the following code produces deadlock. Do you know why?
package main import ( "fmt" "sync" ) func main() { wg := sync.WaitGroup{} wg.Add(2) go printCat(&wg) wg.Wait() go printDog(&wg) wg.Wait() fmt.Println("hello from main") } func printCat(wg *sync.WaitGroup) { for i := 0; i < 5; i++ { fmt.Println("Cat") } wg.Done() } func printDog(wg *sync.WaitGroup){ for i := 0; i < 5; i++ { fmt.Println("Dog") } wg.Done() }
This is because the main GoRoutine has been blocked forever by the first wg.Wait(). We increase the counter of wg by 2 by using wg.Add(2). After the printCat() GoRoutine finishes, the counter remains 1 and blocks the main GoRoutine forever.
Let's have a look at another example to illustrate the power of using GoRoutines to multithread for loop in Go if each iteration is independent to one another.
package main import ( "fmt" "sync" ) func main() { wg := sync.WaitGroup{} for i:=0; i<10; i++{ wg.Add(1) go dosomething(&wg) } wg.Wait() fmt.Println("Finish for loop") } func dosomething(wg *sync.WaitGroup) { fmt.Println("Do something") wg.Done() }
We spawn one GoRoutine at each iteration of the for loop and each GoRoutine will perform a task which is independent to the other tasks. The benefit of using GoRoutines here is that the next iteration does not need to wait for the previous task to finish. We are using WaitGroups to finish the execution of all tasks within the for loop before we execute fmt.Println("Finish for loop").
So far, all GoRoutines have no communication between them. In the next tutorial, we will introduce the concept of Channels which make data sharing between GoRoutines possible.
Stay tuned!