Mutexes in Go

Introduction to Mutexes

In Go, a Mutex (short for "mutual exclusion") is a synchronization primitive used to prevent multiple goroutines from simultaneously accessing shared resources. When a goroutine locks a mutex, other goroutines are blocked from accessing the critical section until the mutex is unlocked.

Using the sync.Mutex Type

The sync.Mutex type is used to create a mutex. The Lock() method is used to acquire the lock, and the Unlock() method is used to release it. Proper handling of the lock ensures that only one goroutine can access the shared resource at a time.

package main

import (
    "fmt"
    "sync"
)

var counter int
var mutex sync.Mutex

func increment() {
    mutex.Lock()
    counter++
    mutex.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)
}

In this example, we use a Mutex to protect access to the shared counter variable. The increment() function locks the mutex before modifying the counter and unlocks it afterward. This ensures that only one goroutine can increment the counter at a time.

Deadlock

A common problem when using mutexes is deadlock, which occurs when two or more goroutines are waiting for each other to release locks, causing the program to become stuck. It's important to carefully manage how locks are acquired and released.

package main

import (
    "fmt"
    "sync"
)

var mutex1 sync.Mutex
var mutex2 sync.Mutex

func task1() {
    mutex1.Lock()
    defer mutex1.Unlock()
    mutex2.Lock()
    defer mutex2.Unlock()
    fmt.Println("Task 1 completed")
}

func task2() {
    mutex2.Lock()
    defer mutex2.Unlock()
    mutex1.Lock()
    defer mutex1.Unlock()
    fmt.Println("Task 2 completed")
}

func main() {
    go task1()
    go task2()
    // Wait for goroutines to finish
    fmt.Scanln()
}

This example introduces a potential deadlock situation. Both task1 and task2 lock two mutexes in a different order, which may lead to a situation where each goroutine is waiting for the other to release a lock, resulting in a deadlock.

Best Practices

To avoid deadlock, you should:

  • Avoid locking multiple mutexes at the same time, or ensure that they are always locked in the same order across all goroutines.
  • Use defer to ensure that mutexes are always unlocked, even in the event of an error or early return from a function.
  • Be mindful of the time your critical section is locked. Keep the critical section as small as possible to minimize contention between goroutines.

Conclusion

Mutexes in Go are powerful tools for synchronizing access to shared resources between multiple goroutines. By using sync.Mutex to lock and unlock critical sections, you can ensure that your programs run safely and avoid data corruption. In the next tutorial, we will explore Go's more advanced synchronization tools, such as the sync.RWMutex for read-write locks.