This article was originally posted at ioscript.org
In the previous article, Introduction to Concurrency in Golang, of the series,Concurrency in Golang, we came across a problem where one or more goroutines might not necessarily finish execution before the main goroutine does, thus we were unable to see the message printed by the unfinished goroutine. We fixed the issue by adding time.Sleep()
in the main goroutine to put it to sleep for a few seconds and thus increasing the probability of other goroutines finishing before the main does. Although our code worked in that particular case, most certainly it will not work for all cases. In this article, we will see, how we can make use of the **WaitGroup **synchronization primitive provided by Go.
Before we discuss WaitGroups let's have a look into Go's concurrency model fork-join.
Fork-Join Model
In concurrency, Fork-Join Model is a way of setting up and executing concurrent programs, such that execution branches off in designated points and joins or merges at a subsequent point and resumes the sequential execution. The word Fork refers to any point of time in the program runtime, it can create one or more child branches of execution to be run either concurrently or in some cases parallel to the parent. The word Join refers to the point in the future when concurrent execution branches
join back to the parent.
WaitGroups
Now, that we know more about the concurrency model, Fork-Join. Let's go through our code from the last article and try to fix it without putting the main goroutine to sleep.
package main
import "fmt"
func sayGreetings() {
fmt.Println("Hello World!!")
}
func main() {
go sayGreetings()
}
When we run the above code, there will be two possibilities. First, the sayGreetings goroutine prints Hello World!
and complete its execution before the main goroutine does. In that case, both Fork and Join operations will happen. In the second case, the sayGreetings goroutine won't be able to complete its execution before the *main * goroutine does. In such a case, the Join operation will not happen.
To solve the above issue, we need to make sure that the Join operation happens in all the cases before the main goroutine completes its execution. We can use synchronization primitive WaitGroup provided by the sync package of Go's inbuilt library.
A WaitGroup **waits for a collection of goroutines to finish. We can think of **WaitGroup as a concurrent-safe counter. WaitGroup provides three methods attached to it.
type WaitGroup
func (wg *WaitGroup) Add(delta int)
func (wg *WaitGroup) Done()
func (wg *WaitGroup) Wait()
func (wg *WaitGroup) Add(delta int)
method increments the counter by the integer passed in. Integer passed into the Add method means the number of new goroutines created.func (wg *WaitGroup) Done()
method decrements the counter by one. We call the Done method when a goroutine completes its execution.func (wg *WaitGroup) Wait()
method is used to block the execution until all the goroutines deployed have already completed their execution.
Let's see how to use WaitGroups in our previous code
package main
import (
"fmt"
"sync"
)
var wg sync.WaitGroup // 1
func sayGreetings() {
defer wg.Done() // 3
fmt.Println("Hello World!!")
}
func main() {
wg.Add(1) // 2
go sayGreetings()
wg.Wait() // 4
}
The above example will deterministically block the main goroutine until the sayGreetings goroutine completes its execution
Explanation
We created a global variable type WaitGroup from the sync package using
var wg sync.WaitGroup
.In the
main
function we callwg.Add(1)
just before we started a goroutinesayGreetings
. This indicates that we have launched one goroutineHere we call
wg.Done()
using defer keyword to make sure thatwg.Done()
is called just before we exit the sayGreetings goroutine closure. This indicates we have completed the execution of one goroutine.Here we call
wg.Wait()
, which will block further execution of themain
goroutine until all the goroutines have completed execution.
Let's see another example on WaitGroups where we launch multiple goroutines.
package main
import (
"fmt"
"sync"
)
var wg sync.WaitGroup
func sayGreetings(i int) {
defer wg.Done()
fmt.Printf("Hello from goroutine %v!!\n", i)
}
func main() {
fmt.Println("Hello from the main goroutine !!")
for i := 0; i < 5; i++ {
wg.Add(1)
go sayGreetings(i)
}
wg.Wait()
fmt.Println("Bye!")
}
Output
Hello from the main goroutine !!
Hello from goroutine 4!!
Hello from goroutine 0!!
Hello from goroutine 1!!
Hello from goroutine 2!!
Hello from goroutine 3!!
Bye !!
We have modified the sayGreetings function to accept an integer value, in order to print the message. Also, we have made certain changes in the main
function to use a for
loop to deploy 5 goroutines to print the message.
It's a good practice to call Add() as closely as possible to the goroutine its tracking. I have called wg.Add(1) just before deploying the goroutine. But you will also find some codes in which wg.Add() is called just before the for
loop, as shown in the below example. Both ways are correct. we just need to make sure that we use the correct integer value for the Add().
const greetingsCount = 5
wg.Add(greetingsCount)
for i:=0; i<greetingsCount; i++ {
go sayGreetings(i)
}
wg.Wait()
Conclusion
In this article, we learned about Go's concurrency model, WaitGroups, and the methods provided with it. We saw how we can use waitgroups to create a join point in our code and make sure all the goroutines deployed get executed completely before the main goroutine exits.
Before You Leave
If you found this article valuable, you can support us by dropping a like and sharing this article with your friends.
You can sign up for our newsletter to get notified whenever we post awesome content on Golang.
Reference
Check our ioscript.org for more such contents.
Top comments (0)