Sobes.tech
Middle

What should be remembered when working with mutexes in Go?

sobes.tech AI

Answer from AI

When working with mutexes (sync.Mutex) in Go, it is important to remember the following:

  • Locking and unlocking: A mutex must be locked (Lock()) before accessing shared data and unlocked (Unlock()) after finishing with it. Using defer mu.Unlock() immediately after mu.Lock() is a good practice to prevent forgetting to unlock.
  • Synchronization granularity: A mutex provides exclusive access to a resource to only one goroutine at a time.
  • "Locked/unlocked" state: Repeatedly calling Lock() on an already locked mutex will block the calling goroutine until the mutex is released by another goroutine. Calling Unlock() on an unlocked mutex causes a panic during execution.
  • Do not copy mutexes: The sync.Mutex struct is not meant to be copied after first use. Passing a mutex by value to a function or copying a struct containing a mutex can lead to incorrect synchronization and panic.
  • Deadlocks: Occur when two or more goroutines wait for each other to release resources. Typical scenario: goroutine A locks resource R1 and waits for R2, while goroutine B locks R2 and waits for R1.
  • Livelocks: Situations where goroutines constantly change their state in response to each other's actions but do not do useful work or make progress. This is less common than deadlocks.
  • Starvation: When one or more goroutines cannot access a shared resource due to constant locking by other goroutines. In Go mutexes, starvation is not guaranteed.
  • Using sync.RWMutex for read/write: If most operations are reads and writes are rare, sync.RWMutex can be more efficient. It allows multiple goroutines to read simultaneously but blocks both reads and writes during a write.
  • Shortest lock durations: Try to keep locks as short as possible. The longer a mutex is locked, the more likely other goroutines will wait, reducing parallelism.
  • Race condition testing: Using the -race flag during build and test runs (go test -race) helps detect data races often caused by improper mutex use.

Example of using defer:

import (
	"sync"
)

type SafeCounter struct {
	mu sync.Mutex
	v  map[string]int
}

// Inc safely increments the counter for a given key.
func (c *SafeCounter) Inc(key string) {
	c.mu.Lock()
	// Defer unlocking until the function exits.
	defer c.mu.Unlock()
	c.v[key]++
}

// Value returns the current value of the counter for a given key.
func (c *SafeCounter) Value(key string) int {
	c.mu.Lock()
	// Defer unlocking after getting the value.
	defer c.mu.Unlock()
	return c.v[key]
}