Зачем в Go нужна синхронизация горутин и какие примитивы для этого существуют?

Ответ

Синхронизация в Go необходима для управления доступом к общим данным из нескольких горутин. Без синхронизации возникает состояние гонки (data race), когда несколько горутин одновременно обращаются к одной и той же области памяти, и хотя бы одна из них выполняет запись. Это приводит к непредсказуемому поведению и трудноуловимым багам.

Основные примитивы синхронизации в пакете sync:

  • sync.Mutex: Мьютекс (mutual exclusion) для защиты "критической секции" — участка кода, который в один момент времени может выполнять только одна горутина. Используется для защиты общего состояния.
  • sync.RWMutex: "Читающий-пишущий" мьютекс. Позволяет неограниченному числу горутин одновременно читать данные, но запись возможна только при эксклюзивной блокировке. Эффективен для сценариев с частым чтением и редкой записью.
  • sync.WaitGroup: Позволяет дождаться завершения работы группы горутин. Основные методы: Add() для увеличения счетчика горутин, Done() для уменьшения счетчика (вызывается в конце работы горутины) и Wait() для блокировки до тех пор, пока счетчик не станет равен нулю.
  • sync.Once: Гарантирует, что определенный участок кода будет выполнен только один раз за все время работы программы. Идеально подходит для инициализации синглтонов.
  • sync.Cond: Условная переменная, которая позволяет горутинам ожидать наступления определенного события или условия.

Каналы (chan) как средство синхронизации:

Каналы — это не только способ передачи данных между горутинами, но и мощный механизм синхронизации. Отправка или получение данных из канала блокирует горутину до тех пор, пока на другом конце канала не появится "партнер".

Ключевой принцип Go:

"Не используйте общую память для коммуникации; вместо этого используйте коммуникацию для обмена памятью." (Don't communicate by sharing memory; share memory by communicating.)

Это означает, что передача данных через каналы часто является более идиоматичным и безопасным подходом, чем использование мьютексов и общего состояния.

Пример с Mutex:

var counter int
var mu sync.Mutex // Мьютекс для защиты counter

func increment(wg *sync.WaitGroup) {
    defer wg.Done()
    mu.Lock() // Блокируем доступ для других горутин
    // Начало критической секции
    counter++
    // Конец критической секции
    mu.Unlock() // Освобождаем блокировку
}