Effective concurrency control is critical for writing safe and performant concurrent programs. This section explores the key mechanisms provided by Go for managing shared state and ensuring correct program behavior: mutexes and atomic operations.
Definition:
Example of Race Condition:
govar counter int
func increment() {
counter++
}
func main() {
for i := 0; i < 1000; i++ {
go increment()
}
time.Sleep(time.Second)
fmt.Println("Final counter value:", counter)
}
counter
variable concurrently, leading to a race condition.Introduction to Mutexes:
Mutex
(mutual exclusion) is a synchronization primitive that ensures only one goroutine can access a critical section of code at a time.Basic Mutex Example:
govar (
counter int
mu sync.Mutex
)
func increment() {
mu.Lock()
counter++
mu.Unlock()
}
func main() {
for i := 0; i < 1000; i++ {
go increment()
}
time.Sleep(time.Second)
fmt.Println("Final counter value:", counter)
}
Types of Mutexes:
Using RWMutex:
govar (
counter int
rwMu sync.RWMutex
)
func readCounter() int {
rwMu.RLock()
defer rwMu.RUnlock()
return counter
}
func writeCounter() {
rwMu.Lock()
counter++
rwMu.Unlock()
}
func main() {
for i := 0; i < 1000; i++ {
go writeCounter()
}
time.Sleep(time.Second)
fmt.Println("Final counter value:", readCounter())
}
Introduction to Atomic Operations:
sync/atomic
package provides functions for atomic operations on various types.Basic Atomic Operation Example:
govar counter int64
func increment() {
atomic.AddInt64(&counter, 1)
}
func main() {
for i := 0; i < 1000; i++ {
go increment()
}
time.Sleep(time.Second)
fmt.Println("Final counter value:", atomic.LoadInt64(&counter))
}
Common Atomic Functions:
atomic.AddInt32
/ atomic.AddInt64
: Atomically adds a value.atomic.LoadInt32
/ atomic.LoadInt64
: Atomically loads a value.atomic.StoreInt32
/ atomic.StoreInt64
: Atomically stores a value.atomic.CompareAndSwapInt32
/ atomic.CompareAndSwapInt64
: Atomically compares and swaps values.Using Compare and Swap (CAS):
govar counter int64
func compareAndSwap(old, new int64) bool {
return atomic.CompareAndSwapInt64(&counter, old, new)
}
func main() {
old := atomic.LoadInt64(&counter)
new := old + 1
if compareAndSwap(old, new) {
fmt.Println("Counter updated to:", new)
} else {
fmt.Println("Counter update failed")
}
}
Minimize Critical Sections:
Prefer Atomic Operations for Simple Counters:
Use RWMutex for Read-Heavy Workloads:
sync.RWMutex
to allow multiple concurrent readers while still ensuring mutual exclusion for writers.Avoid Deadlocks:
Profile and Monitor:
go run -race
) to catch race conditions during development.gotype Counter struct {
mu sync.Mutex
value int64
}
func (c *Counter) Increment() {
c.mu.Lock()
defer c.mu.Unlock()
c.value++
}
func (c *Counter) Value() int64 {
c.mu.Lock()
defer c.mu.Unlock()
return c.value
}
func (c *Counter) AtomicIncrement() {
atomic.AddInt64(&c.value, 1)
}
func (c *Counter) AtomicValue() int64 {
return atomic.LoadInt64(&c.value)
}
func main() {
var c Counter
// Use mutex-protected increment
for i := 0; i < 1000; i++ {
go c.Increment()
}
time.Sleep(time.Second)
fmt.Println("Mutex Counter value:", c.Value())
// Use atomic increment
for i := 0; i < 1000; i++ {
go c.AtomicIncrement()
}
time.Sleep(time.Second)
fmt.Println("Atomic Counter value:", c.AtomicValue())
}
By mastering mutexes and atomic operations, you can effectively manage concurrency in your Go applications, ensuring safe and efficient access to shared resources. These tools help you avoid race conditions and ensure correct program behavior in a concurrent environment.