Sync
Package sync
provides basic synchronization primitives such as mutual exclusion locks. Other than the Once
and WaitGroup
types, most are intended for use by low-level library routines. Higher-level synchronization is better done via channels and communication.
Once
The sync.Once
type ensures that a function is only executed once, regardless of how many goroutines call it. This can be useful for scenarios where you want to perform some initialization or setup.
package main
import (
"fmt"
"sync"
"time"
)
var (
initialized bool
initOnce sync.Once
)
func initialize() {
fmt.Println("Initializing...")
// Perform initialization tasks here
initialized = true
}
func performTask() {
// The function passed to sync.Once.Do will only be executed once,
// even if multiple goroutines call it.
initOnce.Do(initialize)
// Perform the actual task here
fmt.Println("Performing the task...")
}
func main() {
// Run performTask in multiple goroutines
for i := 0; i < 5; i++ {
go performTask()
}
fmt.Println("Waiting for tasks to complete...")
time.Sleep(2 * time.Second)
}
// =>
// Waiting for tasks to complete...
// Initializing...
// Performing the task...
// Performing the task...
// Performing the task...
// Performing the task...
// Performing the task...
WaitGroups
The sync.WaitGroup
is used to wait for a collection of goroutines to finish their execution.
package main
import (
"fmt"
"sync"
"time"
)
func worker(id int, wg *sync.WaitGroup) {
defer wg.Done() // Decrement the counter when the goroutine completes
fmt.Printf("Worker %d started\n", id)
time.Sleep(time.Second) // Simulating some work
fmt.Printf("Worker %d completed\n", id)
}
func main() {
var wg sync.WaitGroup
numWorkers := 3
for i := 1; i <= numWorkers; i++ {
wg.Add(1) // Increment the counter before starting a new goroutine
go worker(i, &wg)
}
// Wait for all goroutines to finish
wg.Wait()
fmt.Println("All workers have completed.")
}
// =>
// Worker 3 started
// Worker 1 started
// Worker 2 started
// Worker 3 completed
// Worker 2 completed
// Worker 1 completed
// All workers have completed.
Atomic Counters
The sync/atomic
package provides atomic operations for basic types, including atomic counters.
Atomic operations are essential for managing shared state in concurrent programs without the need for locks.
package main
import (
"fmt"
"sync"
"sync/atomic"
"time"
)
func incrementCounter(counter *int64, wg *sync.WaitGroup) {
defer wg.Done()
for i := 0; i < 10000; i++ {
// Atomically increment the counter
atomic.AddInt64(counter, 1)
}
time.Sleep(5 * time.Millisecond)
}
func main() {
var counter int64
var wg sync.WaitGroup
numGoroutines := 5
for i := 0; i < numGoroutines; i++ {
wg.Add(1)
go incrementCounter(&counter, &wg)
}
wg.Wait()
fmt.Println("Final Counter Value:", atomic.LoadInt64(&counter))
}
// => Final Counter Value: 50000
Mutexes
A mutex (short for mutual exclusion) is a synchronization primitive that protects shared data from being accessed concurrently by multiple goroutines.
package main
import (
"fmt"
"sync"
)
type Counter struct {
value int
mu sync.Mutex
}
func (c *Counter) Increment() {
c.mu.Lock()
defer c.mu.Unlock()
c.value++
}
func (c *Counter) GetValue() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.value
}
func main() {
var wg sync.WaitGroup
counter := Counter{}
numGoroutines := 5
for i := 0; i < numGoroutines; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for j := 0; j < 10000; j++ {
counter.Increment()
}
}()
}
wg.Wait()
fmt.Println("Final Counter Value:", counter.GetValue())
}
// => Final Counter Value: 50000
Mutexes are suitable for protecting complex shared resources, critical sections, or scenarios where fine-grained control over locking is needed. While atomic counters are suitable for simple scenarios where the shared resource is a single counter or flag.
Code Challenge
Implement a concurrent counter that can be incremented and decremented by multiple goroutines safely using mutexes.