Working with WaitGroups
In the previous lab, we used time.Sleep
to wait for goroutines to complete, but this approach is unreliable. A better way to synchronize goroutines is to use WaitGroups from the sync
package.
What is a WaitGroup?#
A WaitGroup is a synchronization primitive that allows you to wait for a collection of goroutines to finish executing. It provides three main methods:
Add(delta int)
: Adds delta (a positive or negative value) to the WaitGroup counterDone()
: Decrements the WaitGroup counter by 1Wait()
: Blocks until the WaitGroup counter is 0
Basic Pattern#
The basic pattern for using WaitGroups is:
- Create a WaitGroup
- Call
Add
to set the number of goroutines to wait for - Launch goroutines and have each call
Done
when finished - Call
Wait
to block until all goroutines are done
Simple Example#
Here's a simple example of using WaitGroups:
package main
import (
string">"fmt"
string">"sync"
)
func printSomething(s string, wg *sync.WaitGroup) {
string">"comment">// Ensure we call Done when this function returns
defer wg.Done()
fmt.Println(s)
}
func main() {
string">"comment">// Create a WaitGroup
var wg sync.WaitGroup
string">"comment">// Add the number of goroutines we'll wait for
wg.Add(3)
string">"comment">// Launch 3 goroutines
go printSomething(string">"First message", &wg)
go printSomething(string">"Second message", &wg)
go printSomething(string">"Third message", &wg)
string">"comment">// Wait for all goroutines to finish
wg.Wait()
fmt.Println(string">"All goroutines completed")
}
In this example:
- We create a WaitGroup
wg
- We call
wg.Add(3)
to indicate we'll wait for 3 goroutines - We launch three goroutines, each calling
wg.Done()
when finished - We call
wg.Wait()
to block until all goroutines are done
Processing a Collection with WaitGroups#
Here's a more practical example that processes items from a slice:
package main
import (
string">"fmt"
string">"sync"
)
func processItem(i int, item string, wg *sync.WaitGroup) {
defer wg.Done()
fmt.Printf(string">"%d: %s\n", i, item)
}
func main() {
var wg sync.WaitGroup
words := []string{
string">"one",
string">"two",
string">"five",
}
string">"comment">// Add the number of items we need to process
wg.Add(len(words))
string">"comment">// Launch a goroutine for each item
for i, word := range words {
go processItem(i, word, &wg)
}
string">"comment">// Wait for all processing to complete
wg.Wait()
fmt.Println(string">"All items processed")
}
Important Considerations#
1. Always Pass WaitGroups by Pointer#
WaitGroups must be passed by pointer. If you pass them by value, each goroutine gets a copy, and calling Done()
on those copies won't affect the original WaitGroup.
string">"comment">// Correct: passing by pointer
go someFunction(&wg)
string">"comment">// Incorrect: passing by value
go someFunction(wg) string">"comment">// This won't work as expected
2. Call Add
Before Launching Goroutines#
Always call Add
before launching the goroutines to avoid race conditions where Wait
might return before all goroutines have started.
string">"comment">// Correct
wg.Add(3)
go func1(&wg)
go func2(&wg)
go func3(&wg)
string">"comment">// Incorrect - race condition possible
go func1(&wg) string">"comment">// wg.Add(1) might not happen before Wait() checks counter
go func2(&wg)
go func3(&wg)
3. Use defer wg.Done()
#
It's good practice to use defer wg.Done()
at the beginning of your goroutine functions to ensure Done()
is called even if the function panics.
func worker(wg *sync.WaitGroup) {
defer wg.Done() string">"comment">// This will be called even if the function panics
string">"comment">// Do work...
}
4. Balance Add and Done#
Make sure the number of Done()
calls matches the number of increments from Add()
. If you call Done()
too few times, Wait()
will block forever. If you call it too many times, you'll get a panic.
Advanced Example: WaitGroup with Message Passing#
Here's a more advanced example that combines WaitGroups with channels for message passing:
package main
import (
string">"fmt"
string">"sync"
)
func updateMessage(s string, wg *sync.WaitGroup, m *string) {
defer wg.Done()
*m = s
}
func printMessage(m *string) {
fmt.Println(*m)
}
func main() {
var wg sync.WaitGroup
msg := string">"Hello, world!"
wg.Add(1)
go updateMessage(string">"Hello, One!", &wg, &msg)
wg.Wait()
printMessage(&msg)
wg.Add(1)
go updateMessage(string">"Hello, two!", &wg, &msg)
wg.Wait()
printMessage(&msg)
wg.Add(1)
go updateMessage(string">"Hello, three!", &wg, &msg)
wg.Wait()
printMessage(&msg)
}
In this example, we're using WaitGroups to ensure that each message update is completed before printing it.
Summary#
- WaitGroups provide a reliable way to wait for goroutines to complete
- The basic pattern is: Create, Add, Launch goroutines with Done, Wait
- Always pass WaitGroups by pointer
- Call Add before launching goroutines
- Use defer wg.Done() for safety
- Ensure Add and Done calls are balanced
WaitGroups are a fundamental synchronization mechanism in Go, and they're often combined with other concurrency patterns to build robust concurrent programs.
In the next lab, we'll learn how to write tests for concurrent code using WaitGroups.