Синхронизация и 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++ для процессора состоит из трех шагов:
- Прочитать значение
count. - Увеличить его на 1.
- Записать обратно.
Если две горутины прочитают значение
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) — это "общение". Мы передаем данные между горутинами, и тогда никому не нужно захватывать "замки". О них мы поговорим в следующей, завершающей части про горутины.
Что запомнить:
- Используйте
sync.WaitGroupдля ожидания завершения группы горутин. - Используйте
sync.Mutexдля защиты критических участков кода от одновременного изменения данных. - Всегда проверяйте код через
go run -race.