Understanding Mutex in Go
Concurrency is a powerful feature in Go, but managing shared resources can lead to race conditions—situations where multiple goroutines access and modify the same data concurrently, causing unexpected behavior. To handle this safely, Go provides the Mutex (short for “mutual exclusion”) in the sync
package. This article will guide you through the basics of Mutex, its use cases, and practical examples.
What is a Mutex?
A Mutex is a synchronization primitive that ensures only one goroutine can access a critical section of code or a shared resource at a time. This helps avoid race conditions.
Key Methods of sync.Mutex
Lock()
: Acquires the lock. If another goroutine has the lock, the calling goroutine will wait until the lock is released.Unlock()
: Releases the lock, allowing other goroutines to acquire it.
Example: Bank Account Without Mutex
Here’s an example of how a race condition can occur:
package main
import (
"fmt"
"time"
)
var balance int
func deposit(amount int) {
for i := 0; i < amount; i++ {
balance++ // Increment balance
}
}
func main() {
balance = 0
go deposit(1000) // Goroutine 1
go deposit(1000) // Goroutine 2
time.Sleep(1 * time.Second) // Wait for goroutines to finish
fmt.Println("Final Balance:", balance) // May not be 2000!
}
Output
The final balance may not be 2000
due to concurrent writes to the balance
variable.
Fixing the Race Condition With Mutex
Using a sync.Mutex
, we can ensure that only one goroutine updates the balance at a time:
package main
import (
"fmt"
"sync"
"time"
)
var (
balance int
mu sync.Mutex
)
func deposit(amount int, wg *sync.WaitGroup) {
defer wg.Done()
mu.Lock() // Lock the mutex
for i := 0; i < amount; i++ {
balance++ // Safely update balance
}
mu.Unlock() // Unlock the mutex
}
func main() {
balance = 0
var wg sync.WaitGroup
wg.Add(2)
go deposit(1000, &wg) // Goroutine 1
go deposit(1000, &wg) // Goroutine 2
wg.Wait() // Wait for both goroutines to finish
fmt.Println("Final Balance:", balance) // Always 2000
}
Explanation
mu.Lock()
ensures exclusive access to thebalance
variable.mu.Unlock()
releases the lock so other goroutines can proceed.
Embedding Mutex in a Struct
Embedding a Mutex
in a struct is a common pattern for encapsulating synchronization logic with the data it protects.
Example: Thread-Safe Counter
package main
import (
"fmt"
"sync"
)
// Counter struct with embedded Mutex
type Counter struct {
mu sync.Mutex
value int
}
// Increment safely increments the counter
func (c *Counter) Increment() {
c.mu.Lock()
c.value++
c.mu.Unlock()
}
// GetValue safely retrieves the current value
func (c *Counter) GetValue() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.value
}
func main() {
counter := &Counter{}
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
defer wg.Done()
counter.Increment()
}()
}
wg.Wait()
fmt.Println("Final Counter Value:", counter.GetValue()) // Always 100
}
Benefits
- Encapsulation: The
Counter
struct owns both the data and the lock. - Concurrency Safety: The
Mutex
ensures safe access tovalue
.
Advanced Usage: Read-Write Mutex
For scenarios with multiple readers and fewer writers, sync.RWMutex
can be more efficient. It allows multiple readers or a single writer at a time.
Example: Thread-Safe Cache
package main
import (
"fmt"
"sync"
)
// SafeCache is a thread-safe key-value store
type SafeCache struct {
mu sync.RWMutex
store map[string]string
}
// Put stores a key-value pair in the cache
func (c *SafeCache) Put(key, value string) {
c.mu.Lock()
defer c.mu.Unlock()
c.store[key] = value
}
// Get retrieves a value by key
func (c *SafeCache) Get(key string) (string, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
value, exists := c.store[key]
return value, exists
}
func main() {
cache := &SafeCache{store: make(map[string]string)}
var wg sync.WaitGroup
// Writer goroutine
wg.Add(1)
go func() {
defer wg.Done()
cache.Put("greeting", "Hello, World!")
}()
// Reader goroutines
wg.Add(1)
go func() {
defer wg.Done()
if value, exists := cache.Get("greeting"); exists {
fmt.Println("Cache Value:", value)
} else {
fmt.Println("Key not found!")
}
}()
wg.Wait()
}
Key Takeaways
- Mutex is essential for preventing race conditions in concurrent programming.
- Embedding a
Mutex
in a struct helps encapsulate synchronization logic with the data it protects. - Use
sync.RWMutex
for read-heavy workloads to improve performance. - Always use
defer
to ensure locks are released, even if an error occurs or the function exits early.