Go, with its powerful concurrency support, offers a set of utilities that make handling concurrent operations easier and more efficient. In this article, we will explore two important packages: sync and sync/atomic. These packages provide the necessary tools to manage shared variables and synchronize goroutines without the complexity of more traditional mutexes or locks.
The `sync` Package
The sync package is part of Go’s standard library and provides several utilities for concurrent programming:
- Mutex: A basic lock mechanism for ensuring mutual exclusion.
- WaitGroup: Useful for waiting for a collection of goroutines to finish executing.
- Once: Ensures that a particular action is only performed once.
- Cond: Implements conditional variables.
- Pool: A thread-safe way to cache and reuse objects.
Using Mutex
Here’s a simple example to demonstrate how a sync.Mutex can be used to protect a shared variable.
package main
import (
"fmt"
"sync"
)
func main() {
var mu sync.Mutex
var balance int
wg := sync.WaitGroup{}
n := 100
wg.Add(n)
for i := 0; i < n; i++ {
go func() {
mu.Lock()
balance++
mu.Unlock()
wg.Done()
}()
}
wg.Wait()
fmt.Println("Balance:", balance)
}In this code, we set up a sync.Mutex instance, mu, which we lock before updating the balance and unlock afterwards. This ensures that only one goroutine can modify balance at a time.
Using WaitGroup
Next, let’s discuss how you can use a sync.WaitGroup to wait for a collection of goroutines to complete.
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
worker := func(id int) {
fmt.Printf("Worker %d starting\n", id)
// Simulate some work
for i := 0; i < 5; i++ {
fmt.Printf("Worker %d working\n", id)
}
fmt.Printf("Worker %d done\n", id)
wg.Done()
}
workersCount := 3
wg.Add(workersCount)
for i := 1; i <= workersCount; i++ {
go worker(i)
}
wg.Wait()
fmt.Println("All workers done")
}In this example, the sync.WaitGroup is utilized to track when the set of workers is complete by calling wg.Done() for each worker completion.
The `sync/atomic` Package
The sync/atomic package provides low-level atomic memory primitives that load from and store to memory atomically, ensuring operations are thread-safe. This can be especially useful for updating integers or pointers without locks.
Using Atomic Operations
Consider the following example where we increment a counter atomically.
package main
import (
"fmt"
"sync"
"sync/atomic"
)
func main() {
var value int64
wg := sync.WaitGroup{}
n := 100
wg.Add(n)
for i := 0; i < n; i++ {
go func() {
atomic.AddInt64(&value, 1)
wg.Done()
}()
}
wg.Wait()
fmt.Println("Value:", value)
}Here, we use atomic.AddInt64(&value, 1) to safely increment the value without explicit locks, providing a significant performance boost in certain scenarios.
Conclusion
The sync and sync/atomic packages in Go are incredibly powerful and provide the essential tools to effectively harness concurrency. Understanding these packages is crucial to making concurrent programming simpler and more efficient in your Go applications.