Что такое состояние гонки (Race Condition) и как с ним бороться в Go?

Ответ

Состояние гонки (Race Condition) — это ошибка проектирования многопоточной системы, при которой результат работы зависит от непредсказуемого порядка выполнения операций в разных горутинах.

Проблема возникает, когда две или более горутины одновременно обращаются к одной и той же области памяти (общему ресурсу), и хотя бы одна из них выполняет запись.

Пример

Классический пример — инкремент счетчика из нескольких горутин без синхронизации. Операция counter++ неатомарна и состоит из трех шагов: чтение, изменение, запись. Другая горутина может прочитать значение до того, как первая его запишет, что приведет к потере инкремента.

var counter int

func increment() {
    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) // Результат будет < 1000
}

Способы предотвращения в Go

  1. Мьютексы (sync.Mutex): Самый распространенный способ. Мьютекс позволяет только одной горутине получить доступ к критической секции кода.

    var mu sync.Mutex
    mu.Lock()
    counter++
    mu.Unlock()
  2. Каналы: Использование каналов для передачи данных между горутинами, что позволяет избежать общего доступа к памяти. Например, одна горутина может владеть данными и обрабатывать запросы на их изменение, получаемые из канала.

  3. Атомарные операции (sync/atomic): Для простых операций, таких как инкремент, декремент или CompareAndSwap, пакет atomic предоставляет аппаратно-оптимизированные функции, которые выполняются без прерываний. Это эффективнее, чем использование мьютекса.

    atomic.AddInt64(&counter, 1)

Обнаружение

Go имеет встроенный детектор гонок. Чтобы его использовать, нужно запустить программу с флагом -race:

go run -race main.go