WaitGroups in Golang

WaitGroups in Golang

Synchronization Primitives in Go - Part 1

Sonu Kumar Saw
·Feb 19, 2022·

5 min read

Subscribe to our newsletter and never miss any upcoming articles

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.

Frame 2.png

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.

Frame 2 (1).png

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

  1. We created a global variable type WaitGroup from the sync package using var wg sync.WaitGroup.

  2. In the main function we call wg.Add(1) just before we started a goroutine sayGreetings. This indicates that we have launched one goroutine

  3. Here we call wg.Done() using defer keyword to make sure that wg.Done() is called just before we exit the sayGreetings goroutine closure. This indicates we have completed the execution of one goroutine.

  4. Here we call wg.Wait(), which will block further execution of the main 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

Frame 1.png

Did you find this article valuable?

Support ioScript.org by becoming a sponsor. Any amount is appreciated!

See recent sponsors Learn more about Hashnode Sponsors
 
Share this