Примитивы синхронизации Go — дёшево, но не прощают ошибок
«Не общайтесь разделяемой памятью — разделяйте память общением» — известный совет про Go, но он не отменяет того, что мьютексы и атомики в стандартной библиотеке существуют не зря. Канал — отличный инструмент для передачи владения данными, но защитить счётчик или кэш в горячем цикле дешевле обычным sync.Mutex или одной атомарной операцией. Серьёзный Go-разработчик владеет обоими подходами и знает, когда какой уместен.
Сложность пакета sync не в API — Lock/Unlock, Add/Done/Wait, Do выглядят тривиально. Сложность в том, что эти примитивы не прощают ошибок. Скопированная после первого использования структура с мьютексом тихо копирует состояние блокировки. Счётчик WaitGroup, ушедший ниже нуля, роняет программу через panic. Смешанный атомарный и обычный доступ к одной переменной остаётся гонкой данных — неопределённым поведением, которое не ловится исключением. А в основе всего лежит модель памяти Go: без явного ребра happens-before одна goroutine вообще не обязана видеть записи другой. Эта тема разбирает синхронизацию по слоям — от устройства мьютекса до правил видимости записей.
Карта темы
- sync.Mutex — взаимное исключение, нулевое значение как готовый мьютекс, нереентерабельность и режим starvation для честности.
- sync.WaitGroup — ожидание набора goroutine, контракт
Add/Done/Waitи почемуAddобязан опережать запуск goroutine. - Пакет sync/atomic — операции без блокировок над одним машинным словом и типизированные обёртки
atomic.Int64/atomic.Value. - Атомики на уровне CPU —
LOCK-префикс на x86, пара load-linked/store-conditional на ARM и барьеры памяти. - sync.RWMutex — много читателей ЛИБО один писатель, выигрыш на read-heavy нагрузке и защита писателя от голодания.
- sync.Once — гарантия однократного выполнения через атомарный быстрый путь и медленный путь под мьютексом.
- Модель памяти Go — правила happens-before, рёбра синхронизации каналов и мьютексов, отсутствие порядка без них.
- Атомики против мьютекса — когда хватает одной неделимой операции, а когда нужна полноценная критическая секция.
Частые ошибки и ловушки
| Ошибка | Последствие |
|---|---|
Скопировать структуру с sync.Mutex после первого использования | Копируется состояние блокировки; go vet ловит это, но не всегда читают вывод |
Считать sync.Mutex реентерабельным | Повторный Lock на своём мьютексе в той же goroutine — deadlock |
Вызвать Add внутри уже запущенной goroutine | Гонка с Wait; счётчик может ещё не учитывать goroutine |
Передать WaitGroup по значению в goroutine | Done обновляет копию; Wait блокируется навсегда |
Увести счётчик WaitGroup ниже нуля лишним Done | panic — программа падает |
| Смешать атомарный и обычный доступ к одной переменной | Гонка данных остаётся; неопределённое поведение |
| Защищать атомиками многошаговый инвариант | Атомик покрывает лишь одно слово; промежуточное состояние видно другим goroutine |
Брать RWMutex по умолчанию | При низкой конкуренции он медленнее обычного sync.Mutex |
| Считать, что записи видны между goroutine без ребра синхронизации | Несвежие чтения или гонка — модель памяти не даёт порядка без happens-before |
Полагать, что выровненный MOV атомарен между ядрами сам по себе | Без LOCK-префикса и барьера другие ядра не увидят результат согласованно |
Значение для собеседований
Синхронизация — обязательная тема на любом серьёзном Go-интервью. Каналы кандидат обычно знает; пакет sync и модель памяти отделяют того, кто умеет рассуждать о корректности конкурентного кода, от того, кто заучил go func(){}().
Что обычно проверяют:
- Что даёт
sync.Mutex, почему его нельзя копировать и почему он не реентерабельный. - Контракт
Add/Done/Waitи почемуAddобязан выполниться до запуска goroutine. - Что предоставляет
sync/atomicи почему атомик защищает лишь одно машинное слово. - Как атомарная операция реализована на уровне CPU —
LOCK-префикс, LL/SC, барьеры памяти. - Когда
sync.RWMutexвыигрывает уsync.Mutexи какова его цена. - Как
sync.Onceгарантирует однократный запуск и почему ему нужны и атомик, и мьютекс. - Что такое happens-before и какие рёбра синхронизации его устанавливают.
Типичный неверный ответ: «атомики — это просто быстрый мьютекс, ими можно защитить что угодно». Это запускает разбор того, что атомик неделимо обновляет ровно одно слово, а согласованность нескольких значений или многошаговая критическая секция требуют sync.Mutex.