26 вопросов
Mutex, RWMutex, WaitGroup, Once, Cond, Map, Pool. Также Locker - интерфейс Lock()/Unlock(). Нет семафора в стандартной библиотеке - делают через канал или сторонние пакеты.
sync.Mutex - обычный взаимоисключающий мьютекс. sync.RWMutex - read-write mutex: много читателей или один писатель. Оба реализованы в пакете sync. С Go 1.18 Mutex реализован с учётом очереди ожидания и режима голодания.
RWMutex позволяет нескольким горутинам держать блокировку на чтение одновременно; запись эксклюзивна. Mutex в любой момент держит только одна горутина. RWMutex выгоден при частом чтении и редкой записи; при сравнимой нагрузке чтение/запись Mutex может быть проще и быстрее.
Mutex - защита общей памяти, критическая секция. RWMutex - когда много читателей, редко пишут. Канал - передача данных и сигналов между горутинами, pipeline. Для простой защиты переменной - Mutex; для передачи владения данными - канал.
Когда записей много или критические секции короткие - на RWMutex есть накладные расходы на управление читателями. Когда конкуренция низкая. Когда код проще с одним Mutex. Бенчмарки на конкретной нагрузке покажут лучше.
Add(delta) увеличивает счётчик (обычно вызывают до запуска горутин). Done() уменьшает счётчик на 1 (вызывают в конце горутины, часто через defer wg.Done()). Wait() блокируется, пока счётчик не станет 0. Add с отрицательным delta или когда счётчик уже 0 может вызвать панику.
var wg sync.WaitGroup
wg.Add(1)
go func() { defer wg.Done(); work() }()
wg.Wait()Если вызвать Add(1) внутри запускаемой горутины, планировщик может выполнить Wait() до того, как Add успеет выполниться - Wait увидит 0 и не будет ждать новую горутину. Add до запуска гарантирует, что счётчик увеличен до того, как кто-то вызовет Wait.
wg.Add(len(tasks))
for _, t := range tasks { go func(t T) { defer wg.Done(); t.Do() }(t) }
wg.Wait()Once гарантирует, что функция выполнится ровно один раз, даже при множественных вызовах из разных горутин. once.Do(f) вызывает f() при первом вызове; последующие вызовы блокируются до завершения первого и больше f не вызывают.
var once sync.Once
once.Do(func() { initSingleton() })Условная переменная: ожидание события с разблокировкой мьютекса. Wait() атомарно разблокирует мьютекс и блокирует горутину; при пробуждении снова захватывает мьютекс. Signal() будит одну, Broadcast() - всех. Используют для ожидания условия под мьютексом (очередь, готовность данных).
cond := sync.NewCond(&mu)
// waiter: mu.Lock(); for !condition { cond.Wait() }; ...; mu.Unlock()
// signaller: mu.Lock(); condition=true; cond.Signal(); mu.Unlock()Когда ключи в основном только растут и редко удаляются, либо когда разные горутины работают с непересекающимися наборами ключей. sync.Map оптимизирован под эти сценарии. Для обычного кеша с частыми записями/удалениями часто выгоднее map + Mutex/RWMutex.
sync.Map - специализированная структура с внутренней блокировкой, подходит для двух сценариев выше. map + Mutex - проще предсказать производительность, полный контроль. При высокой конкуренции по одним и тем же ключам map+Mutex может быть быстрее. Выбор по профилю доступа.
Пул объектов для переиспользования и снижения давления на GC. Get() возвращает объект из пула (или новый), Put(obj) возвращает объект в пул. Пул может очищаться в любой момент; объекты не привязаны к горутине. Используют для временных буферов, парсеров и т.п.
var bufPool = sync.Pool{New: func() any { return new(bytes.Buffer) }}
buf := bufPool.Get().(*bytes.Buffer)
defer buf.Reset(); defer bufPool.Put(buf)Пакет sync/atomic: Add, CompareAndSwap (CAS), Load, Store для типов int32, int64, uint32, uint64, uintptr. atomic.Value для произвольного типа (Store/Load). Нет атомарных операций для float и сложных структур - только через Value или Mutex.
var n int64
atomic.AddInt64(&n, 1)
atomic.CompareAndSwapInt64(&n, 0, 1)atomic - для простых счётчиков и флагов, одна переменная, минимум contention. Mutex - для сложных инвариантов, нескольких полей, критических секций. atomic быстрее при низкой конкуренции; при сложной логике и нескольких полях Mutex проще и безопаснее.
В sync семафора нет. Реализуют через буферизованный канал (отправка - захват, приём - освобождение) или через golang.org/x/sync/semaphore. В Go 1.21+ есть runtime/sem для внутреннего использования, но для пользовательского кода обычно канал или semaphore.Weighted.
Spinlock - ожидание в цикле (busy-wait), пока блок не освободится. Mutex в Go при contention переводит горутину в ожидание и отдаёт поток другим. В Go явного spinlock в стандартной библиотеке нет; Mutex внутри может кратко "крутиться" перед сном. Обычно используют Mutex.
Да. Mutex можно использовать как "барьер": одна горутина Lock/Unlock, другая Lock - вторая дождётся первой. Так синхронизируют порядок событий. Но для сигналов чаще удобнее канал или Cond.
В Go 1.9+ Mutex может переходить в режим голодания (starvation mode): если горутина ждёт блокировку "долго", следующая разблокировка отдаёт блокировку ожидающему, а не текущему владельцу. Это снижает риск того, что ожидающие горутины никогда не получат блокировку при активном конкуренте.
Паника распространяется из Do к вызывающему. Once считает, что f не выполнилась успешно - следующий вызов Do снова вызовет f. Чтобы считать "однократное выполнение" даже при панике, нужно recover внутри f и как-то сохранить результат (например, в переменную).
errgroup.Group - это WaitGroup плюс контекст и накопление первой ошибки. Запуск через g.Go(f), ожидание g.Wait() возвращает ошибку. При первой ошибке контекст отменяется. Удобно для параллельных задач с отменой и возвратом ошибки. WaitGroup - только ожидание завершения без ошибок и контекста.
Интерфейс с методами Lock() и Unlock(). Его реализуют *sync.Mutex и *sync.RWMutex. Используют когда функции принимают "любой примитив блокировки", например для условной блокировки или обёрток.
func withLock(l sync.Locker, f func()) { l.Lock(); defer l.Unlock(); f() }Mutex содержит внутреннее состояние (состояние блокировки, очередь). Копия - отдельный мьютекс; блокировка одной копии не блокирует другую. Защита данных будет сломана. Передавать по указателю. То же для RWMutex, WaitGroup, Once и др.
Пакет golang.org/x/sync/singleflight: группирует одновременные вызовы с одним ключом в один реальный вызов; все ждущие получают один результат. Устраняет "thundering herd" при кешировании: много запросов одного ключа - один запрос в бэкенд.
var g singleflight.Group
v, err, _ := g.Do("key", func() (interface{}, error) { return fetch(key) })Тип из sync/atomic для атомарного хранения и загрузки значения произвольного типа. Store(interface{}) и Load() interface{}. После первого Store тип фиксируется - Store другого типа приведёт к панике. Используют для конфигурации "copy-on-write", счётчиков структур и т.п.
var config atomic.Value
config.Store(newConfig)
cfg := config.Load().(Config)Объекты в пуле могут быть в любой момент удалены сборщиком мусора; пул не даёт гарантий хранения. Обычно пул очищается при GC. Get может вернуть объект из пула или новый. Put возвращает объект в пул. Не полагаться на то, что Put сохранит объект надолго.
Возвращает sync.Locker, который при Lock/Unlock вызывает RLock/RUnlock того же RWMutex. Нужно когда функция принимает sync.Locker, но вы хотите передать только право на чтение (многие горутины могут держать RLock одновременно).
func readWithLock(l sync.Locker, f func()) { l.Lock(); defer l.Unlock(); f() }
readWithLock(mu.RLocker(), func() { _ = data[key] })