sync: Mutex, RWMutex, WaitGroup, Once, Cond, Pool и Map
Пакет sync предоставляет низкоуровневые механизмы координации. Их выбирают по инварианту: Mutex защищает изменяемое состояние, WaitGroup ждёт завершения, Once выполняет инициализацию один раз. Примитив нельзя копировать после первого использования, а его корректность определяется дисциплиной всех участников.
Зачем это на интервью
Интервьюер ожидает, что вы не предложите RWMutex без профиля, не вызовете Add конкурентно с начавшимся Wait, а также понимаете, почему sync.Map и sync.Pool — не универсальные замены map и allocation.
Минимум для E4
- Держать под lock весь инвариант и отпускать lock через
deferв небольшой критической секции. - Вызывать
wg.Addдо запуска goroutine, аDone— черезdeferвнутри неё. - Использовать
RWMutexтолько когда есть преимущественно читатели и измеренная конкуренция. - Не копировать
Mutex,RWMutex,WaitGroup,Once,Cond,Poolпосле использования.
package main
import (
"fmt"
"sync"
)
type Counter struct {
mu sync.Mutex
n int
}
func (c *Counter) Add(delta int) {
c.mu.Lock()
defer c.mu.Unlock()
c.n += delta
}
func (c *Counter) Value() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.n
}
func main() {
var wg sync.WaitGroup
var c Counter
for range 100 {
wg.Add(1) // до go
go func() {
defer wg.Done()
c.Add(1)
}()
}
wg.Wait()
fmt.Println(c.Value())
}Углубление для E5/Senior
Mutex устанавливает happens-before от Unlock к последующему Lock, поэтому защищённые данные не требуют atomic. RWMutex разрешает параллельных readers, но усложняет путь записи и может проигрывать Mutex при коротких секциях. Lock не держат во время сетевого вызова, callback или потенциально долгой операции; порядок нескольких locks фиксируют глобально для предотвращения deadlock.
Once публикует результаты f всем вернувшимся из Do, но не является retry-механизмом: если f panic, Do считается завершённым. Cond нужен для сложного ожидания condition под lock и всегда ждёт в цикле; чаще яснее канал. Pool — кэш временных объектов, который GC может очистить в любой момент; объект возвращают только после полного прекращения использования. sync.Map уместен для append-only/read-mostly или независимых ключей, но не даёт атомарного составного инварианта между ключами.
Ключевые понятия
- Critical section — код, который читает/меняет общий инвариант под lock.
- Mutex/RWMutex — взаимное исключение;
RLockне разрешает запись. - WaitGroup — счётчик завершений; не средство cancellation и не reusable без завершённого предыдущего Wait.
- Once — один успешный вызов
Doна экземпляр. - Cond — condition variable:
Waitатомарно отпускает lock и засыпает. - Pool/Map — специализированные concurrent структуры с ограниченной семантикой.
Типовые вопросы
- Почему
WaitGroupне отменяет goroutine?- Он лишь ждёт счётчик. Для остановки нужен context, закрытие канала или другой сигнал, который goroutine сама наблюдает.
- Когда
RWMutexхужеMutex?- При частых writes, коротких секциях или малой конкуренции его накладные расходы превосходят выгоду параллельных reads.
- Можно ли вызвать
AddпослеWait?- Нельзя добавлять положительный счётчик, когда
Waitуже ждёт и счётчик нулевой; для нового цикла дождитесь завершения предыдущего и настройте группу до запуска работ.
- Нельзя добавлять положительный счётчик, когда
- Зачем
Cond.Waitв цикле?- Пробуждение не означает, что predicate всё ещё истинный; condition проверяют под тем же lock после каждого wakeup.
- Почему
sync.Poolне cache с гарантиями?- Runtime может очистить pool при GC, а объект нельзя использовать после
Put; нельзя строить на нём корректность или обязательное сохранение.
- Runtime может очистить pool при GC, а объект нельзя использовать после
- Когда выбрать
sync.Map?- При его специализированных паттернах: ключ записывается раз и потом читается многими, либо разные goroutine работают с непересекающимися ключами. Для общего инварианта — map+mutex.
Практика
- Защитите map и связанный счётчик одним mutex. Готово: инвариант
count == len(map)сохраняется подgo test -race. - Реализуйте
WaitGroup-запуск workers. Готово:Addпроисходит доgo, каждый путь worker вызываетDoneровно раз. - Сравните
MutexиRWMutexbenchmark на read-heavy и write-heavy нагрузке. Готово: вывод основан наgo test -bench, а не предположении.
Частые ошибки и ловушки
- Читать защищённое mutex поле без mutex «потому что запись редкая».
- Копировать struct с mutex value receiver или возвращать его по значению.
- Вызывать внешний код под lock.
- Использовать
WaitGroupкак semaphore или хранить в нём бизнес-состояние. - Применять
sync.Mapдля нескольких зависимых ключей.
Связанные темы
Конкурентность · Атомарные операции · Гонки и deadlock · Worker pool