Waitgroups in Golang a Detailed guide

Introduction

WaitGroups in Go (Golang) is a powerful synchronization primitive provided by the sync package. It ensures that your program waits for a collection of goroutines to finish executing, preventing premature exits or progression while goroutines are still running. Before diving into WaitGroups in Golang, it’s essential to understand the basics of goroutines, as they are the fundamental building blocks of concurrent execution in Go. Understanding goroutines will provide you with the necessary context to effectively use WaitGroups for managing and synchronizing your concurrent tasks.

Let’s learn about wait groups with an easy example using goroutines. We’ll see how wait groups help manage goroutines smoothly.

Goroutines

In Go (Golang), goroutines play a crucial role in enabling concurrent programming, making it possible to execute multiple tasks simultaneously. Goroutines, in essence, are managed by the Go runtime and are lightweight threads, which enable functions or methods to run concurrently with others while being more resource-efficient compared to traditional operating system threads.

Key Characteristics

  1. Lightweight: Goroutines consume much less memory than traditional threads. When a goroutine is created, it uses only a few kilobytes of stack space, which can grow and shrink as needed.

  2. Efficient Scheduling: The Go runtime includes a scheduler that manages goroutines. It efficiently multiplexes goroutines onto a smaller number of operating system threads.

  3. Simple Syntax: Creating a goroutine is as simple as prefixing a function call with the go keyword.

Creating Goroutines

To start a goroutine, you use the go keyword followed by a function call. For example:

package main

import (
   "fmt"
   "time"
)

func main() {

   fmt.Println("Main Goroutine started")
   go Greeting("John")
   go Greeting("James")
   go Greeting("Michael")
   fmt.Println("Main Goroutine completed")
}

func Greeting(name string) {
   now := time.Now()
   hour := now.Hour()

   switch {
   case hour >= 12 && hour < 17: // 12:00 PM to 5:00 PM
       fmt.Printf("Good afternoon, %v\n !", name)
   case hour < 12: // before 12:00 PM
       fmt.Printf("Good morning, %v\n !", name)
   case hour >= 17 && hour < 22: // after 5:00 PM
       fmt.Printf("Good evening, %v\n !", name)
   default:
       fmt.Printf("Good night, %v\n ! It is time to sleep ! ", name)
   }
}
// output :Main Goroutine started
           Main Goroutine completed

For detail program, please visit my GitHub repository

Explanation 

Main Goroutine Execution:

  • The main function serves as the starting point of the program and runs in the main goroutine.
  • When executed, the main goroutine uses the fmt.Println(“Main Goroutine started”) statement to print “Main Goroutine started” to the console.
  • After that, the main goroutine starts three instances of the Greeting function in separate goroutines with the go keyword.
  • Once the greeting goroutines are triggered, the main goroutine continues to execute the following lines of code and prints “Goroutines completed” to the console. It’s important to note that the main goroutine finishes its execution and exits without waiting for goroutines execution.

Greeting Goroutine Execution:

  • Each greeting goroutine operates concurrently alongside the main goroutine and other greeting goroutines.
  • Within each greeting goroutine, the Greeting function executes. It retrieves the current time using time.Now() and obtains the current hour via now.Hour().
  • Utilizing a switch statement, the Greeting function determines the current time of day and outputs an appropriate greeting message accordingly.

Program Termination:

  • After spawning the greeting goroutines, it prints “Main Goroutine completed” to the console and the main part of the program quickly exits without waiting for invoked goroutine’s execution. 
  • When the main function/goroutine exits, the entire program terminates, causing all concurrent goroutines i.e. greeting goroutines to stop. This could result in some greeting messages being missed if the program stops before they’re all executed.

Concurrency in Go:

  • This program demonstrates concurrency in Go, where multiple goroutines (the main goroutine and the greeting goroutines) can run concurrently and independently. 
  • The main goroutine does not wait for the greeting goroutines to complete their execution unless explicit synchronization is used, such as sync.WaitGroup or channels.

You may wonder why goroutines responses are not printed to the console in the above program, as these goroutines run concurrently and independently, the main goroutine does not wait as the behaviour of the go runtime scheduler is to switch the execution of a goroutine to another when blocking statement occurs. 

(Note : “When the main function in the main goroutine ends, it immediately stops any other goroutines it started. This happens because the Go runtime exits the program as soon as the main function finishes, regardless of any running goroutines.”)

Lets us include the time.Sleep(time.Second) in the main function in above program such as

func main() {
   fmt.Println("Goroutines started")
   go Greeting("John")
   go Greeting("James")
   go Greeting("Michael")
   time.Sleep(time.Second)
}
Output : 	
Goroutines started
Good afternoon, John!
Good afternoon, Michael!
Good afternoon, James!

In this updated version of the program, we’ve added a 1-second delay using time.Sleep(time.Second) after initiating the goroutines in the main function. This deliberate delay pauses the main goroutines execution for 1 second. This allows one of the greeting goroutines to start the execution. So Greeting(“John”), Greeting(“James”), and Greeting(“Michael”) will finish their tasks before the program exits.
It’s crucial to understand that while time.Sleep can pause the execution temporarily, it doesn’t serve as a synchronization on its own. For ensuring that all greeting goroutines complete their execution before the program exits, we recommend utilizing synchronization like sync.WaitGroup or channels.

Let us understand goroutine synchronization in Golang using sync package waitgroups.

Waitgroups example

Wait groups are a core concept in Go’s concurrency model. They enable a goroutine to pause and wait until one or more other goroutines complete their tasks, ensuring synchronization and coordination between them.

A sync.WaitGroup in the sync package is a struct which includes the Add() method that maintains a counter for the number of goroutines. When a goroutine finishes its task, it calls the Done() method on the WaitGroup, which decrements the counter. Once the counter reaches zero, It indicates that all goroutines have completed their execution.

Here’s a breakdown of the WaitGroup methods:


1. Add(delta int):   Increments the counter by delta. This is usually called before starting a new goroutine to tell the WaitGroup how many goroutines it should wait for.

wg.Add(1)  // Increment the counter by 1
wg.Add(3)  // Increment the counter by 3
Note : You can adjust the WaitGroup counter to match the number of goroutines you intend to invoke.

2. Done():   Decrements the counter by 1. This method is called by a goroutine when it finishes its task. It is equivalent to calling wg.Add(-1).

defer wg.Done()  // Signal completion of this goroutine

3. Wait():  Blocks the calling goroutine until the counter reaches 0. This is used to wait for all the goroutines to complete before moving on.

wg.Wait()  // Block until the counter reaches 0

Lets us understand by modifying the above example ,

package main

import (
   "fmt"
   "sync"
   "time"
)

func main() {
   var wg sync.WaitGroup
   fmt.Println("Goroutines started")

   wg.Add(3)// we have 3 goroutines , so we added 3 to add()

   go Greeting("John", &wg)
   go Greeting("James", &wg)
   go Greeting("Michael", &wg)

   wg.Wait()

   fmt.Println("All goroutines completed")
}

func Greeting(name string, wg *sync.WaitGroup) {
   defer wg.Done()
   now := time.Now()
   hour := now.Hour()
   switch {
   case hour >= 12 && hour < 17: // 12:00 PM to 5:00 PM
       fmt.Printf("Good afternoon, %v!\n", name)
   case hour < 12: // before 12:00 PM
       fmt.Printf("Good morning, %v!\n", name)
   case hour >= 17 && hour < 22: // after 5:00 PM
       fmt.Printf("Good evening, %v!\n", name)
   default:
       fmt.Printf("Good night, %v!\nIt's time to sleep!\n", name)
   }
}

For detail program, please visit my GitHub repository

  1. Packages imported:
    • fmt for formatted printing, sync for synchronization and time for working with time-related operations
  2. Main Function:
    • The program starts with the main function, serving as its entry point.
    • To handle synchronization between goroutines, a wait group instance wg is initialized from the sync package.
    • Upon program start, the console prints “Goroutines started.”
    • By using the wg method, the wait group instance wg is incremented by 3 to signify the expectation of three goroutines to finish before continuing.
    • Using the go keyword and the Greeting function, three goroutines are created, each with a distinct name parameter (“John”, “James”, “Michael”).
    • Concurrently, the main function waits for all three goroutines to complete by using wg.wait().
  3. Greeting Goroutine:
    • In the Greeting  goroutine, two parameters are required: a name string and a WaitGroup() pointer (wg).
    • Within the function, the defer statement is utilized to reduce the wait group counter when the function finishes, indicating that the goroutine has completed its task.(Note: defer is employed to execute the statement wg.Done()  at the conclusion of the goroutine’s execution.)
    • The goroutine acquires the current time by using  time.Now() and retrieves the hour through now.Hour().
    • It employs a switch statement to determine the appropriate greeting based on the current hour.
  4. Program Output:
    • When all three goroutines complete their execution (each greeting a person with their name), the program prints “All goroutines completed” to the console.

Overall, this program demonstrates concurrent execution using goroutines and synchronization using waitgroups to ensure that all goroutines finish their tasks before the main program exits.

Custom waitgroup

package main

import (
   "fmt"
   "sync/atomic"
)

type custWaitGroup struct {
   count int64
}

func (wg *custWaitGroup) Add(n int64) {
   atomic.AddInt64(&wg.count, n)
}

func (wg *custWaitGroup) Done() {
   wg.Add(-1)
   if atomic.LoadInt64(&wg.count) < 0 {
       panic("negative wait group counter")
   }
}

func (wg *custWaitGroup) Wait() {
   for atomic.LoadInt64(&wg.count) != 0 {
       continue
   }
}

func main() {
   var wg custWaitGroup
   wg.Add(2)
   go func() {
       defer wg.Done()
       fmt.Println("GoroutinesTwo")
   }()
   go func() {
       defer wg.Done()
       fmt.Println("GoroutineOne")
   }()
   wg.Wait()
   fmt.Println("All goroutines have completed their execution")
}

Please visit my Github Repository for detail program.

The provided code defines a custom custWaitGroup type using atomic operations to manage goroutine synchronization. The Add method increments a counter, Done decrements it and checks for negative values, and Wait busy-waits until the counter reaches zero. In the main function, two goroutines are spawned, each calling Done when finished. The Wait method ensures the main function waits until both goroutines complete before printing a final message.