Перейти к основному содержимому

Синхронизация и Data Races

Когда две или более горутины одновременно обращаются к одной и той же переменной, и хотя бы одна из них пытается её изменить, возникает гонка данных (data race). Это состояние — "мины замедленного действия": программа может работать годами, а потом однажды упасть или начать выдавать случайные ошибки.

1. Проблема: Data Race

Посмотрите на этот код. Мы запускаем 1000 горутин, каждая из которых пытается увеличить счетчик.

func main() {
count := 0
for i := 0; i < 1000; i++ {
go func() {
count++ // Опасная операция!
}()
}
fmt.Println("Итог:", count)
}

Результат: Скорее всего, вы не получите 1000. Вы получите любое число меньше 1000. Почему? Операция count++ для процессора состоит из трех шагов:

  1. Прочитать значение count.
  2. Увеличить его на 1.
  3. Записать обратно. Если две горутины прочитают значение 0 одновременно, они обе прибавят 1 и запишут 1, хотя должны были записать 2. Одна единица была "потеряна".

2. Инструмент №1: sync.WaitGroup (Ждем завершения)

В прошлом примере мы вывели результат до того, как горутины успели выполниться. Чтобы дождаться их всех, используем WaitGroup.

  • Add(1): Говорим: "Я запустил одну задачу, жди её".
  • Done(): Говорим: "Я закончил задачу, вычитай из счетчика".
  • Wait(): Блокирует выполнение main, пока счетчик не станет равен 0.
import "sync"

func main() {
var wg sync.WaitGroup
count := 0

for i := 0; i < 1000; i++ {
wg.Add(1) // Добавляем задачу в очередь
go func() {
defer wg.Done() // Выполнится в конце функции
count++
}()
}

wg.Wait() // Ждем, пока все 1000 горутин не вызовут Done()
fmt.Println("Итог:", count)
}

3. Инструмент №2: sync.Mutex (Защита данных)

Даже с WaitGroup программа выше выдаст неверный результат, потому что count++ всё ещё подвержен гонке данных. Нам нужно "захватить" переменную, чтобы никто другой не мог её изменить, пока мы это делаем. Для этого используем Mutex (взаимное исключение).

import "sync"

func main() {
var wg sync.WaitGroup
var mu sync.Mutex // Мьютекс для защиты
count := 0

for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()

mu.Lock() // Захватываем "замок"
count++ // Теперь только эта горутина может менять count
mu.Unlock() // Отпускаем "замок"
}()
}

wg.Wait()
fmt.Println("Итог:", count) // Теперь всегда 1000
}

4. Как найти ошибки? (Data Race Detector)

Ошибки гонки данных сложно найти вручную. В Go есть встроенный инструмент для их обнаружения. Просто запустите программу с флагом -race:

go run -race main.go

Если в коде есть конфликты при доступе к данным, Go сам напишет, в каких строках происходит конфликт. Используйте этот флаг всегда при тестировании!

Золотое правило конкурентности в Go

В Go есть знаменитая фраза:

"Don't communicate by sharing memory; share memory by communicating." (Не общайтесь через разделение памяти; разделяйте память через общение.)

  • Мутекс (Mutex) — это "разделение памяти". Он нужен, когда горутины "дерутся" за одну переменную. Это эффективно, но требует осторожности.
  • Каналы (chan) — это "общение". Мы передаем данные между горутинами, и тогда никому не нужно захватывать "замки". О них мы поговорим в следующей, завершающей части про горутины.

Что запомнить:

  1. Используйте sync.WaitGroup для ожидания завершения группы горутин.
  2. Используйте sync.Mutex для защиты критических участков кода от одновременного изменения данных.
  3. Всегда проверяйте код через go run -race.