The sync package in Go provides fundamental synchronization primitives such as mutual exclusion locks, condition variables, and wait groups. These tools are essential for managing concurrent access to shared resources and ensuring that your Go programs run smoothly without data races or deadlocks.
Concurrency is a core feature of the Go language, allowing you to write efficient and scalable applications by executing multiple tasks simultaneously. However, when multiple goroutines access shared resources, it's crucial to synchronize their execution to prevent race conditions and ensure data consistency.
The sync package offers several synchronization primitives that help manage concurrent access:
In this tutorial, we'll explore each of these primitives in detail, providing real-world examples and best practices to help you write robust concurrent Go programs.
A Mutex (mutual exclusion lock) is a synchronization primitive that allows only one goroutine to access a shared resource at a time. This prevents race conditions by ensuring exclusive access.
Here's a simple example of using a Mutex:
package main
import (
"fmt"
"sync"
)
var (
counter int
mu sync.Mutex
)
func increment(wg *sync.WaitGroup) {
defer wg.Done()
mu.Lock() // Lock the mutex before accessing the shared resource
counter++
fmt.Println("Counter:", counter)
mu.Unlock() // Unlock the mutex after modifying the shared resource
}
func main() {
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go increment(&wg)
}
wg.Wait()
fmt.Println("Final counter value:", counter)
}
defer to handle unlocking automatically.An RWMutex (read-write lock) allows multiple readers or a single writer to access a shared resource. This is more efficient than using a regular mutex in scenarios where reads are much more frequent than writes.
Here's an example of using an RWMutex:
package main
import (
"fmt"
"sync"
)
var (
data map[string]int
rwMutex sync.RWMutex
)
func readData(key string, wg *sync.WaitGroup) {
defer wg.Done()
rwMutex.RLock() // Lock for reading
value := data[key]
fmt.Println("Read", key, ":", value)
rwMutex.RUnlock() // Unlock after reading
}
func writeData(key string, value int, wg *sync.WaitGroup) {
defer wg.Done()
rwMutex.Lock() // Lock for writing
data[key] = value
fmt.Println("Wrote", key, ":", value)
rwMutex.Unlock() // Unlock after writing
}
func main() {
var wg sync.WaitGroup
data = make(map[string]int)
// Simulate multiple reads and writes
for i := 0; i < 10; i++ {
wg.Add(2)
go readData("key", &wg)
go writeData("key", i, &wg)
}
wg.Wait()
}
RWMutex when you have a high ratio of reads to writes.A WaitGroup is used to wait for a collection of goroutines to finish executing. It's particularly useful when you need to coordinate the termination of multiple goroutines.
Here's an example of using a WaitGroup:
package main
import (
"fmt"
"sync"
)
func worker(id int, wg *sync.WaitGroup) {
defer wg.Done()
fmt.Printf("Worker %d starting\n", id)
// Simulate work with sleep
time.Sleep(time.Second)
fmt.Printf("Worker %d done\n", id)
}
func main() {
var wg sync.WaitGroup
for i := 1; i <= 5; i++ {
wg.Add(1) // Increment the WaitGroup counter
go worker(i, &wg)
}
wg.Wait() // Wait for all workers to finish
fmt.Println("All workers done")
}
Add before starting a goroutine and Done when the goroutine completes.Done.Channels are another fundamental synchronization primitive in Go. They allow goroutines to communicate and synchronize their execution.
Here's an example of using channels:
package main
import (
"fmt"
"time"
)
func worker(id int, jobs <-chan int, results chan<- int) {
for j := range jobs {
fmt.Printf("Worker %d started job %d\n", id, j)
time.Sleep(time.Second)
fmt.Printf("Worker %d finished job %d\n", id, j)
results <- j * 2
}
}
func main() {
const numJobs = 5
jobs := make(chan int, numJobs)
results := make(chan int, numJobs)
for w := 1; w <= 3; w++ {
go worker(w, jobs, results)
}
for j := 1; j <= numJobs; j++ {
jobs <- j
}
close(jobs) // Close the jobs channel to signal no more jobs
for a := 1; a <= numJobs; a++ {
fmt.Println("Result:", <-results)
}
}
The sync package in Go provides essential tools for managing concurrency and synchronizing access to shared resources. By understanding and correctly using mutexes, RWMutexes, WaitGroups, and channels, you can write robust and efficient concurrent programs.
Remember to always follow best practices such as locking and unlocking mutexes correctly, using buffered channels appropriately, and avoiding deadlocks. With these tools at your disposal, you'll be well-equipped to handle complex concurrency scenarios in Go.