Go: Синхронизация (sync)

26 вопросов

1 Какие примитивы есть в пакете sync?

Mutex, RWMutex, WaitGroup, Once, Cond, Map, Pool. Также Locker - интерфейс Lock()/Unlock(). Нет семафора в стандартной библиотеке - делают через канал или сторонние пакеты.

Открыть отдельно →
2 Какие бывают типы Mutex?

sync.Mutex - обычный взаимоисключающий мьютекс. sync.RWMutex - read-write mutex: много читателей или один писатель. Оба реализованы в пакете sync. С Go 1.18 Mutex реализован с учётом очереди ожидания и режима голодания.

Открыть отдельно →
3 RWMutex vs Mutex?

RWMutex позволяет нескольким горутинам держать блокировку на чтение одновременно; запись эксклюзивна. Mutex в любой момент держит только одна горутина. RWMutex выгоден при частом чтении и редкой записи; при сравнимой нагрузке чтение/запись Mutex может быть проще и быстрее.

Открыть отдельно →
4 Когда что использовать: Mutex, RWMutex, канал?

Mutex - защита общей памяти, критическая секция. RWMutex - когда много читателей, редко пишут. Канал - передача данных и сигналов между горутинами, pipeline. Для простой защиты переменной - Mutex; для передачи владения данными - канал.

Открыть отдельно →
5 Когда Mutex лучше RWMutex?

Когда записей много или критические секции короткие - на RWMutex есть накладные расходы на управление читателями. Когда конкуренция низкая. Когда код проще с одним Mutex. Бенчмарки на конкретной нагрузке покажут лучше.

Открыть отдельно →
6 Как работают WaitGroup Add, Done, Wait?

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()
Открыть отдельно →
7 Почему wg.Add лучше вызывать до цикла, а не внутри горутины?

Если вызвать 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()
Открыть отдельно →
8 Как работает sync.Once?

Once гарантирует, что функция выполнится ровно один раз, даже при множественных вызовах из разных горутин. once.Do(f) вызывает f() при первом вызове; последующие вызовы блокируются до завершения первого и больше f не вызывают.

var once sync.Once
once.Do(func() { initSingleton() })
Открыть отдельно →
9 Что такое sync.Cond?

Условная переменная: ожидание события с разблокировкой мьютекса. 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()
Открыть отдельно →
10 Когда использовать sync.Map?

Когда ключи в основном только растут и редко удаляются, либо когда разные горутины работают с непересекающимися наборами ключей. sync.Map оптимизирован под эти сценарии. Для обычного кеша с частыми записями/удалениями часто выгоднее map + Mutex/RWMutex.

Открыть отдельно →
11 sync.Map vs map + Mutex?

sync.Map - специализированная структура с внутренней блокировкой, подходит для двух сценариев выше. map + Mutex - проще предсказать производительность, полный контроль. При высокой конкуренции по одним и тем же ключам map+Mutex может быть быстрее. Выбор по профилю доступа.

Открыть отдельно →
12 Что такое sync.Pool?

Пул объектов для переиспользования и снижения давления на 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)
Открыть отдельно →
13 Какие атомарные операции есть в Go?

Пакет 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)
Открыть отдельно →
14 atomic vs Mutex?

atomic - для простых счётчиков и флагов, одна переменная, минимум contention. Mutex - для сложных инвариантов, нескольких полей, критических секций. atomic быстрее при низкой конкуренции; при сложной логике и нескольких полях Mutex проще и безопаснее.

Открыть отдельно →
15 Есть ли семафор в стандартной библиотеке?

В sync семафора нет. Реализуют через буферизованный канал (отправка - захват, приём - освобождение) или через golang.org/x/sync/semaphore. В Go 1.21+ есть runtime/sem для внутреннего использования, но для пользовательского кода обычно канал или semaphore.Weighted.

Открыть отдельно →
16 Spinlock vs Mutex?

Spinlock - ожидание в цикле (busy-wait), пока блок не освободится. Mutex в Go при contention переводит горутину в ожидание и отдаёт поток другим. В Go явного spinlock в стандартной библиотеке нет; Mutex внутри может кратко "крутиться" перед сном. Обычно используют Mutex.

Открыть отдельно →
17 Можно ли использовать Mutex только для синхронизации без защиты данных?

Да. Mutex можно использовать как "барьер": одна горутина Lock/Unlock, другая Lock - вторая дождётся первой. Так синхронизируют порядок событий. Но для сигналов чаще удобнее канал или Cond.

Открыть отдельно →
18 Что такое starvation mode у Mutex?

В Go 1.9+ Mutex может переходить в режим голодания (starvation mode): если горутина ждёт блокировку "долго", следующая разблокировка отдаёт блокировку ожидающему, а не текущему владельцу. Это снижает риск того, что ожидающие горутины никогда не получат блокировку при активном конкуренте.

Открыть отдельно →
19 Паника внутри sync.Once.Do?

Паника распространяется из Do к вызывающему. Once считает, что f не выполнилась успешно - следующий вызов Do снова вызовет f. Чтобы считать "однократное выполнение" даже при панике, нужно recover внутри f и как-то сохранить результат (например, в переменную).

Открыть отдельно →
20 errgroup.Group vs WaitGroup?

errgroup.Group - это WaitGroup плюс контекст и накопление первой ошибки. Запуск через g.Go(f), ожидание g.Wait() возвращает ошибку. При первой ошибке контекст отменяется. Удобно для параллельных задач с отменой и возвратом ошибки. WaitGroup - только ожидание завершения без ошибок и контекста.

Открыть отдельно →
21 Что такое sync.Locker?

Интерфейс с методами Lock() и Unlock(). Его реализуют *sync.Mutex и *sync.RWMutex. Используют когда функции принимают "любой примитив блокировки", например для условной блокировки или обёрток.

func withLock(l sync.Locker, f func()) { l.Lock(); defer l.Unlock(); f() }
Открыть отдельно →
22 Почему нельзя копировать sync.Mutex?

Mutex содержит внутреннее состояние (состояние блокировки, очередь). Копия - отдельный мьютекс; блокировка одной копии не блокирует другую. Защита данных будет сломана. Передавать по указателю. То же для RWMutex, WaitGroup, Once и др.

Открыть отдельно →
23 Что такое singleflight?

Пакет golang.org/x/sync/singleflight: группирует одновременные вызовы с одним ключом в один реальный вызов; все ждущие получают один результат. Устраняет "thundering herd" при кешировании: много запросов одного ключа - один запрос в бэкенд.

var g singleflight.Group
v, err, _ := g.Do("key", func() (interface{}, error) { return fetch(key) })
Открыть отдельно →
24 Что такое atomic.Value?

Тип из sync/atomic для атомарного хранения и загрузки значения произвольного типа. Store(interface{}) и Load() interface{}. После первого Store тип фиксируется - Store другого типа приведёт к панике. Используют для конфигурации "copy-on-write", счётчиков структур и т.п.

var config atomic.Value
config.Store(newConfig)
cfg := config.Load().(Config)
Открыть отдельно →
25 Жизненный цикл объектов в sync.Pool?

Объекты в пуле могут быть в любой момент удалены сборщиком мусора; пул не даёт гарантий хранения. Обычно пул очищается при GC. Get может вернуть объект из пула или новый. Put возвращает объект в пул. Не полагаться на то, что Put сохранит объект надолго.

Открыть отдельно →
26 Для чего RWMutex.RLocker()?

Возвращает 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] })
Открыть отдельно →
🧠Квиз 🏆Лидеры 🎯Собесед. 📖Вопросы 📚База зн.