Introduction
In Go, a sync.Mutex
(short for mutual exclusion) is a synchronization primitive that provides a way to prevent race conditions by ensuring that only one goroutine can access a critical section of code at a time. A mutex can be locked and unlocked, allowing you to protect shared resources from concurrent access. In this chapter, you will learn the basics of using a mutex in Go, including how to lock and unlock it, common use cases, and best practices.
Using Mutex
Basic Example
To use a sync.Mutex
, you need to create a mutex and then use the Lock
and Unlock
methods to protect critical sections of your code.
Example:
package main
import (
"fmt"
"sync"
)
var (
counter int
mu sync.Mutex
)
func increment() {
mu.Lock()
counter++
mu.Unlock()
}
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 value:", counter) // Output should be 1000
}
In this example, the increment
function locks the mutex before incrementing the counter and unlocks it afterward. This ensures that only one goroutine can increment the counter at a time.
Example: Protecting Multiple Critical Sections
Example:
package main
import (
"fmt"
"sync"
)
type SafeCounter struct {
mu sync.Mutex
v map[string]int
}
func (c *SafeCounter) Inc(key string) {
c.mu.Lock()
c.v[key]++
c.mu.Unlock()
}
func (c *SafeCounter) Value(key string) int {
c.mu.Lock()
defer c.mu.Unlock()
return c.v[key]
}
func main() {
c := SafeCounter{v: make(map[string]int)}
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
c.Inc("somekey")
}()
}
wg.Wait()
fmt.Println("Final count for 'somekey':", c.Value("somekey")) // Output should be 1000
}
In this example, the SafeCounter
struct uses a mutex to protect access to the map v
. The Inc
and Value
methods both lock the mutex to ensure safe concurrent access.
Recursive Locking
Go’s sync.Mutex
does not support recursive locking. This means that if a goroutine tries to lock a mutex it already holds, it will cause a deadlock.
Example: Deadlock Due to Recursive Locking
Example:
package main
import (
"sync"
)
var mu sync.Mutex
func recursiveFunction() {
mu.Lock()
defer mu.Unlock()
// Attempting to lock the mutex again will cause a deadlock
mu.Lock()
defer mu.Unlock()
}
func main() {
go recursiveFunction()
}
In this example, attempting to lock the mutex a second time within the same goroutine causes a deadlock.
Best Practices
-
Use
defer
to Unlock: Always usedefer
to ensure that the mutex is unlocked, even if a function returns early or panics.Example:
func increment() { mu.Lock() defer mu.Unlock() counter++ }
-
Minimize Critical Section Size: Keep the critical section (the code between
Lock
andUnlock
) as small as possible to reduce contention.Example:
func increment() { mu.Lock() counter++ mu.Unlock() }
-
Avoid Recursive Locking: Do not attempt to lock a mutex recursively in the same goroutine.
-
Use RWMutex for Read-Heavy Workloads: If you have a read-heavy workload, consider using
sync.RWMutex
, which allows multiple readers but only one writer.
Example: Using sync.RWMutex
Example:
package main
import (
"fmt"
"sync"
)
type SafeCounter struct {
mu sync.RWMutex
v map[string]int
}
func (c *SafeCounter) Inc(key string) {
c.mu.Lock()
c.v[key]++
c.mu.Unlock()
}
func (c *SafeCounter) Value(key string) int {
c.mu.RLock()
defer c.mu.RUnlock()
return c.v[key]
}
func main() {
c := SafeCounter{v: make(map[string]int)}
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
c.Inc("somekey")
}()
}
wg.Wait()
fmt.Println("Final count for 'somekey':", c.Value("somekey")) // Output should be 1000
}
In this example, the SafeCounter
struct uses a sync.RWMutex
to allow concurrent read access with the RLock
method and exclusive write access with the Lock
method.
Conclusion
The sync.Mutex
in Go is used for ensuring safe concurrent access to shared resources. By understanding how to use mutexes correctly and following best practices, you can avoid race conditions and ensure that your concurrent programs behave as expected. Additionally, for read-heavy workloads, consider using sync.RWMutex
to optimize performance by allowing multiple concurrent readers.