Go Mutex

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

  1. Use defer to Unlock: Always use defer to ensure that the mutex is unlocked, even if a function returns early or panics.

    Example:

    func increment() {
        mu.Lock()
        defer mu.Unlock()
        counter++
    }
    
  2. Minimize Critical Section Size: Keep the critical section (the code between Lock and Unlock) as small as possible to reduce contention.

    Example:

    func increment() {
        mu.Lock()
        counter++
        mu.Unlock()
    }
    
  3. Avoid Recursive Locking: Do not attempt to lock a mutex recursively in the same goroutine.

  4. 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.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top