Что такое состояние гонки (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

Ответ 18+ 🔞

А, слушай, вот тебе про гонки, блядь. Это не когда ты на тачке с мужиками на светофоре соревнуешься, а когда у тебя в программе горутины начинают друг другу мозги ебать, пытаясь к одной и той же переменной одновременно добраться.

Суть в чём, ёпта: представь, у тебя есть общая тарелка с печеньками — это твоя память. И есть два обдолбанных программиста — это горутины. Оба хотят взять печеньку и при этом сказать всем, сколько печенек осталось. Если они полезют одновременно, один возьмёт, посчитает, что осталось девять, а второй, не глядя, тоже возьмёт и тоже напишет «девять». А на деле-то уже восемь, блядь! Вот это и есть состояние гонки — результат зависит от того, кто какого хуя раньше руку протянул, и это непредсказуемо.

Особенно пиздец наступает, когда кто-то не просто читает, а ещё и пишет. Чистое чтение — ещё ладно, а вот запись — это уже война.

Наглядный пиздец

Смотри, классический пример — счётчик. Кажется, что 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. Где-то 956, где-то 998... Пиздец, а не гонка.
}

Как не дать им друг друга переебать

  1. Мьютексы (sync.Mutex) — это как табличка «Занято» на сортире. Одна горутина вешает, делает свои дела, потом снимает. Пока висит — все остальные ждут, как лохи.

    var mu sync.Mutex // Наш верный замок, блядь
    mu.Lock()         // Вешаем табличку
    counter++         // Спокойно жрём печеньку, никто не мешает
    mu.Unlock()       // Сняли, пусть следующий идёт
  2. Каналы — это уже философия, ёпта. Вместо того чтобы всем лезть к одной печеньке, ты заводишь одного ответственного бармена. Все желающие пишут ему записки в канал: «Дай печеньку» или «Доложи печенек». А бармен один всё обрабатывает. Никаких общих данных — одна горутина владеет всем, остальные общаются через каналы. Умно, но иногда овердохуища кода.

  3. Атомарные операции (sync/atomic) — это когда тебе настолько похуй на изящество, что ты просто берёшь и делаешь операцию за один такт процессора, без возможности прерваться. Для счётчиков — самое то. Быстрее, чем мьютекс, но только для простых чисел и булов.

    atomic.AddInt64(&counter, 1) // Раз — и готово. Никаких тебе трёх шагов, всё за один удар.

Как это всё отловить, пока не стало поздно

А вот тут Go — молодец, ёбаный в рот. В нём прямо в коробке лежит детектор гонок. Запускаешь программу с флагом -race, и он тебе подсвечивает все подозрительные места, где горутины могут друг другу ебальника набросить.

go run -race main.go

Выдаст тебе красивый отчёт, где, кто и на какой строке пытался конкурировать. Пользуйся, не будь мудаком, проверяй свой код. А то потом ночью будешь сидеть и думать: «И почему же у меня баланс пользователей постоянно на три рубля схуячивается?».