Synchronization Primitives GoLang
The sync and sync/atomic packages in Go provide the synchronization primitives, which provide conventional multithreading operations and mechanisms for coordinating concurrently running goroutines. When goroutines need to wait for a group of other goroutines to finish their execution, primitives like sync.WaitGroup
are crucial, even though channels are the recommended method for managing concurrency and synchronization in Go.
Explanation
An sync.WaitGroup
waits for a group of goroutines to complete. It functions as a counter with increment and decrement capabilities. To make sure that every goroutine that has been started has finished its tasks, the main goroutine might then block until this counter hits zero. The issue of the main process ending before other goroutines had an opportunity to complete their task is resolved by this.
Methods for synchronization
The sync.WaitGroup
type is a structure that includes fields like noCopy
, state1
(which holds a counter), and sema
. It provides three key methods for synchronization:
Add(delta int)
: Using this method, the WaitGroup’s internal counter is increased by delta. In order to prevent race circumstances, it is essential to call Add(1)
before to beginning a new goroutine.
Done()
: By doing this, the WaitGroup’s counter is lowered by one. Usually, it is called inside the goroutine function using defer to guarantee that it is carried out regardless of how the goroutine departs.
Wait()
: The goroutine that invokes this method is blocked until the WaitGroup’s counter drops to zero. This guarantees that other goroutines won’t stop running while the program doesn’t end too soon.
Code Example:
An illustration of how to use sync.WaitGroup to make sure all goroutines finish their tasks is provided here:
package main
import (
"flag"
"fmt"
"sync" // Import the sync package
)
func main() {
n := flag.Int("n", 20, "Number of goroutines") // Define a command-line flag for the number of goroutines
flag.Parse() // Parse the command-line arguments
count := *n
fmt.Printf("Going to create %d goroutines.\n", count) //
var waitGroup sync.WaitGroup // Declare a sync.WaitGroup variable
// Print the initial state of the WaitGroup (for debugging/understanding)
fmt.Printf("%#v\n", waitGroup) //
for i := 0; i < count; i++ { // Loop to create multiple goroutines
waitGroup.Add(1) // Increment the WaitGroup counter before starting each goroutine
go func(x int) { // Start a new goroutine
defer waitGroup.Done() // Decrement the counter when the goroutine finishes
fmt.Printf("%d ", x)
}(i) // Pass 'i' as an argument to avoid closure issues
}
// Print the state of the WaitGroup after adding all goroutines
fmt.Printf("%#v\n", waitGroup) //
waitGroup.Wait() // Block until all goroutines have called Done()
fmt.Println("\nExiting...") // This line will only execute after all goroutines are done
}
Output
Going to create 20 goroutines.
sync.WaitGroup{noCopy:sync.noCopy{}, state:atomic.Uint64{_:atomic.noCopy{}, _:atomic.align64{}, v:0x0}, sema:0x0}
0 4 3 5 6 1 8 7 9 10 2 sync.WaitGroup{noCopy:sync.noCopy{}, state:atomic.Uint64{_:atomic.noCopy{}, _:atomic.align64{}, v:0x900000000}, sema:0x0}
19 11 12 13 14 15 16 17 18
Exiting...
Explanation of the Example:
flag.Int
and flag.Parse()
: The example is made dynamic by these lines, which let the user define how many goroutines to construct using a command-line input.
var waitGroup sync.WaitGroup
: A variable called WaitGroup is created. Its unlocked WaitGroup value is zero.
waitGroup.Add(1)
: Add(1) is called inside the for loop before each goroutine is launched. By doing this, the WaitGroup’s internal counter is increased, indicating that one more goroutine has to do its task. The purpose of placing this call before the go statement is to avoid race situations in which a goroutine might complete before Add(1) is called.
defer waitGroup.Done()
: Each anonymous goroutine should have a defer waitGroup.Done()
is used. The Done()
call is scheduled to run ahead of the return of the surrounding function, in this case the anonymous goroutine function. This guarantees that, whether the goroutine ends normally or in a panic, the WaitGroup counter is decremented.
waitGroup.Wait()
: Once all goroutines have been launched, waitGroup is the next step in the main function.Wait() is invoked. Until the WaitGroup’s internal counter drops to zero, all goroutines that used Add(1) have called Done(). This call blocks the main goroutine.
Output and state1
: The internal state of the WaitGroup is displayed via the fmt.Printf(“%#v”, waitGroup) instructions. When displayed using %#v, you may see that the counter that rises and falls with Add() and Done() calls is stored in one of the elements in the state1 byte array inside the WaitGroup struct.
The output of the program will display the numbers that the goroutines have printed in a non-deterministic order since the Go scheduler decides the order in which the goroutines execute simultaneously. But the “Exiting…” message will always show up last, ensuring that all of the goroutines have finished.
Potential Issues
Deadlock: A deadlock will result if Add()
is performed more frequently than Done()
, causing the Wait()
function to block indefinitely. A “fatal error: all goroutines are asleep – deadlock!” notice will appear when the application stalls, for instance, if you run Add(1)
more than once without a corresponding Done()
.
Panic: Done()
will generate a panic with the message “sync: negative WaitGroup counter” if it is called more frequently than Add().
To prevent these problems, it is necessary to balance the quantity of Add()
and Done()
calls through careful design and development. Conventional shared memory programming with mutexes is frequently challenging and vulnerable to errors such as race situations. In contrast, Go promotes memory sharing through channels, which are typically faster and safer. But sync.WaitGroup
is a useful feature in situations when waiting for a set of actions to finish is the primary issue.
You can also read What Is Multiple Goroutines In GoLang With Code Examples