Synchronization is crucial in concurrent programming to ensure safe access to shared resources and coordinate the execution of multiple goroutines. Go provides several synchronization primitives, including Mutex
, RWMutex
, WaitGroup
, and Once
, which help manage concurrency effectively.
A Mutex
(mutual exclusion lock) ensures that only one goroutine can access a critical section of code at a time, preventing race conditions.
gopackage main
import (
"fmt"
"sync"
)
var (
counter int
mu sync.Mutex
)
func increment() {
mu.Lock()
defer mu.Unlock()
counter++
}
func main() {
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
increment()
}()
}
wg.Wait()
fmt.Println("Final Counter:", counter) // Output: Final Counter: 1000
}
mu.Lock()
and mu.Unlock()
ensure that only one goroutine at a time can execute the critical section.defer mu.Unlock()
guarantees the mutex is unlocked when the function exits, even if an error occurs.An RWMutex
(read-write mutex) allows multiple readers but only one writer, optimizing for read-heavy workloads.
gopackage main
import (
"fmt"
"sync"
)
var (
data = make(map[int]int)
rwMu sync.RWMutex
wg sync.WaitGroup
)
func write(key, value int) {
rwMu.Lock()
defer rwMu.Unlock()
data[key] = value
}
func read(key int) int {
rwMu.RLock()
defer rwMu.RUnlock()
return data[key]
}
func main() {
// Writing data
for i := 0; i < 5; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
write(i, i*i)
}(i)
}
wg.Wait()
// Reading data
for i := 0; i < 5; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
fmt.Printf("data[%d] = %d\n", i, read(i))
}(i)
}
wg.Wait()
}
rwMu.Lock()
and rwMu.Unlock()
ensure exclusive write access.rwMu.RLock()
and rwMu.RUnlock()
allow multiple concurrent reads.A WaitGroup
waits for a collection of goroutines to finish executing.
gopackage main
import (
"fmt"
"sync"
)
func worker(id int, wg *sync.WaitGroup) {
defer wg.Done()
fmt.Printf("Worker %d starting\n", id)
// Simulate work
fmt.Printf("Worker %d done\n", id)
}
func main() {
var wg sync.WaitGroup
for i := 1; i <= 5; i++ {
wg.Add(1)
go worker(i, &wg)
}
wg.Wait()
fmt.Println("All workers done")
}
wg.Add(1)
increments the WaitGroup counter.wg.Done()
decrements the counter when a goroutine completes.wg.Wait()
blocks until the counter is zero, ensuring all goroutines have finished.A Once
ensures a piece of code runs only once, regardless of how many goroutines invoke it.
gopackage main
import (
"fmt"
"sync"
)
var once sync.Once
func initialize() {
fmt.Println("Initialization done")
}
func worker(wg *sync.WaitGroup) {
defer wg.Done()
once.Do(initialize)
fmt.Println("Worker running")
}
func main() {
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go worker(&wg)
}
wg.Wait()
fmt.Println("All workers done")
}
once.Do(initialize)
ensures initialize
runs only once, even if called by multiple goroutines.Synchronization in Go is essential for managing concurrency and ensuring safe access to shared resources. Mutex
and RWMutex
provide mechanisms for exclusive and shared access, respectively, while WaitGroup
and Once
help coordinate the execution of multiple goroutines. By mastering these synchronization primitives, you can write robust and efficient concurrent programs in Go.