Skip to content

Race Condition

In Go, a race condition occurs when two or more goroutines access and modify the same data concurrently, leading to unpredictable and often erroneous behavior. Go provides several tools to prevent race conditions and ensure the integrity of shared data, including Mutexes, Read-Write Locks, and Atomic Operations.

go run -race

The -race flag in Go is used to enable the built-in data race detector. Data races occur when two or more goroutines access the same memory location concurrently, and at least one of the accesses is a write. This can lead to unpredictable behavior and hard-to-debug issues. It's typically used for testing and debugging, not for running production code.

run command
go test -race mypkg   // test the package
go run -race mysrc.go // compile and run the program
go build -race mycmd  // build the command
go install -race mypkg // install the package

Mutexes (Mutual Exclusion)

A Mutex, short for mutual exclusion, is the most basic form of concurrency control in Go. It allows only one goroutine to access a critical section of code at a time.

Example

The shared counter variable, which multiple goroutines increment concurrently. The sync.Mutex ensures that only one goroutine can execute the increment function at a time by locking and unlocking the Mutex before and after the critical section.

run command
go run src/multithereading/mutex.go
package main

import (
    "fmt"
    "sync"
)

var (
    counter int
    mu      sync.Mutex
)

func increment() {
    mu.Lock()
    defer mu.Unlock()
    counter++
}

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("Counter: ", counter)
}
output
Counter:  1000

Read-Write Locks

Read-Write Locks are a type of lock that can be held by multiple readers but only one writer at a time. They are useful when you have many read operations and few write operations.

In Go, the sync.RWMutex type provides a read-write lock that allows multiple goroutines to read data simultaneously but ensures exclusive access for writing. This is useful in scenarios where you have many read operations and few write operations.

Example

The sync.RWMutex protect access to the data map. Multiple goroutines can read the data using RLock(), allowing for concurrent reading. When writing data, we use Lock() to ensure exclusive access, preventing any concurrent reads or writes.

run command
go run src/multithereading/rwmutex.go
package main

import (
    "fmt"
    "sync"
    "time"
)

var (
    data  map[string]string
    mutex sync.RWMutex
)

func init() {
    data = make(map[string]string)
    data["key1"] = "value1"
    data["key2"] = "value2"
}

func readData(key string) string {
    mutex.RLock()
    defer mutex.RUnlock()
    return data[key]
}

func writeData(key, value string) {
    mutex.Lock()
    defer mutex.Unlock()
    data[key] = value
}

func main() {
    var wg sync.WaitGroup
    for i := 0; i < 5; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            fmt.Println("Read Data:", readData("key1"))
        }()
    }
    time.Sleep(time.Second) // Let the readers start first

    for i := 0; i < 2; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            writeData("key1", "newvalue1")
            fmt.Println("Write Data: key1 -> newvalue1")
        }()
    }

    wg.Wait()
}
output
Read Data: value1
Read Data: value1
Read Data: value1
Read Data: value1
Read Data: value1
Write Data: key1 -> newvalue1
Write Data: key1 -> newvalue1

Atomic Operations

While Mutexes and Read-Write Locks provide high-level abstractions for concurrency control, Go also offers atomic operations for more fine-grained control over shared variables. These operations allow you to perform read-modify-write operations on variables atomically, without the need for locks.

Example

The atomic.AddInt32 atomically increment the counter variable. Since atomic operations are executed without locks, they are highly efficient and are suitable for scenarios where fine-grained control is needed with minimal overhead.

run command
go run src/multithereading/atomic.go
package main

import (
    "fmt"
    "sync"
    "sync/atomic"
)

var (
    counter int32
    wg      sync.WaitGroup
)

func increment() {
    atomic.AddInt32(&counter, 1)
    wg.Done()
}

func main() {
    const numGoroutines = 1000
    wg.Add(numGoroutines)

    for i := 0; i < numGoroutines; i++ {
        go increment()
    }
    wg.Wait()
    fmt.Println("Counter: ", counter)
}
output
Counter:  1000

Choosing the Right Concurrency Control Mechanism

When it comes to choosing the right concurrency control mechanism in Go, you need to consider the specific requirements of your application. Mutexes are suitable for scenarios where only one goroutine should access a critical section at a time. Atomic operations are more efficient but can be more difficult to use correctly. In some cases, a combination of these mechanisms may be beneficial.

References

  1. Concurrency Control in Go: Mutexes, Read-Write Locks, and Atomic Operations
  2. Introducing the Go Race Detector
  3. Data Race Detector