Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Senior ~20 мин 7 вопросов

sync-примитивы на собеседовании Go: Once, RWMutex, Map, Cond, Weighted

За вопросами «чем RWMutex лучше Mutex» стоит проверка понимания примитивов на уровне механики: как устроен sync.Once изнутри, когда sync.Map действительно быстрее и почему это почти никогда. Ниже — вопросы senior-секций с ответами; база конкурентности — в темах горутины и каналы и модель памяти.

Как отвечать: определение → как работает внутри → где ловушка → пример. Отвечайте вслух до чтения ответа — собеседование это устный жанр, и «понимаю про себя» не равно «объясняю словами».

1. Как устроен sync.Once и почему копия Once — баг?

sync.Once держит атомарный флаг и мьютекс: первый Do выполняет функцию под мьютексом и ставит флаг атомарно; остальные Do видят флаг и выходят. Гарантия сильная: функция выполнится РОВНО один раз, даже при панике внутри (начиная с Go 1.21 проваленная паникой инициализация считается выполненной — позже обсудили и смягчили, знать о споре стоит). Копирование Once — классическая ошибка: копия получает снимок флага, и оба «Once» выполнят функцию независимо. Поэтому Once живут по указателю или внутри структуры, которая не копируется.

type Client struct {
    once sync.Once
    conn *Conn
}
func (c *Client) get() *Conn {
    c.once.Do(func() { c.conn = connect() }) // корректно: через указатель
    return c.conn
}

2. Чем RWMutex отличается от Mutex и когда RLock НЕ помогает?

RWMutex допускает много параллельных читателей и одного писателя: RLock берётся многими, Lock — эксклюзивно. Выигрыш реален только при read-heavy нагрузке и достаточно долгих критических секциях: на коротких операциях накладные расходы RWMutex (атомарные счётчики читателей) съедают выгоду, и обычный Mutex быстрее. Ловушки: (1) RLock не апгрейдится до Lock — попытка взять Lock под RLock = дедлок; (2) writer starvation в старых версиях — современные реализации ставят ожидающего писателя барьером для новых читателей. Практика: начинать с Mutex, переходить на RWMutex по профилю с реальной read-heavy нагрузкой.

3. Когда sync.Map действительно быстрее? Почему не «всегда»?

sync.Map оптимизирован под два сценария: (1) ключи записываются один раз, читаются много раз (кэши конфигурации, реестры); (2) горутины работают с непересекающимися наборами ключей. Устройство: чтение идёт по атомарному указателю на read-слот без блокировки, записи перестраивают read/dirty. Цена: записи дороже, len() O(n), range по снимку. Для обычных нагрузок «пишем и читаем вперемешку» мьютекс + обычная карта быстрее и предсказуемее. Ответ-формула: sync.Map — не замена map+Mutex, а специализированный кейс, подтверждённый профилем.

4. Что делает sync.Cond и почему его почти не используют?

Cond — условная переменная: горутины ждут сигнала (Wait) о «условие могло измениться» и просыпаются от Broadcast/Signal. Требует мьютекс-пару и цикл проверки условия (Wait может проснуться ложно — спууриос-пробуждение; поэтому всегда for !condition { c.Wait() }). Почти не используется, потому что те же задачи чище решаются каналами (закрытие канала будит всех — как Broadcast) и контекстом. Знать Cond стоит для чтения чужого кода (стандартная библиотека использует) и для вопросов «как бы вы реализовали блокирующую очередь без каналов».

c := sync.NewCond(&mu)
// ждущий:
mu.Lock()
for queue.IsEmpty() { c.Wait() }
job := queue.Pop()
mu.Unlock()
// добавляющий:
mu.Lock(); queue.Push(job); c.Broadcast(); mu.Unlock()

5. Что такое Weighted (golang.org/x/sync/semaphore) и чем лучше канального семафора?

Weighted — семафор с весами: Acquire(ctx, n) занимает n единиц, Release(n) возвращает; полезен, когда ресурсы неоднородны — например, скачивание файлов весом в размере (мелкие файлы не занимают «слот» целиком). Канальный семафор из темы про паттерны даёт только единичные слоты. Плюс Weighted: Acquire принимает контекст — ожидание слота отменяется вместе с запросом; канальному варианту для этого нужен select. В ответе стоит назвать и лимит: чем больше весов в системе, тем вероятнее нехватка из-за дробления — веса стоит подбирать с запасом.

sem := semaphore.NewWeighted(100) // например, 100 МБ лимита
err := sem.Acquire(ctx, sizeOf(file)) // вес = размер файла
defer sem.Release(sizeOf(file))

6. Mutex — справедливый? Может ли горутина вечно ждать Lock?

Mutex Go не строго FIFO: рантайм балансирует пропускную способность и справедливость — новый захват в момент освобождения может «проскочить» мимо очереди ждущих (если мьютекс в незаблокированном «разогретом» состоянии), но голодание писателей/читателей бесконечно не допускается: после порога ожидания сквозняки прекращаются, и очередь честно раздаётся. Аналогичная история с RWMutex: писатель блокирует новых читателей. Практический вывод: экстремальная конкуренция за один мьютекс — сигнал дробить состояние или пересматривать дизайн, а не искать «справедливый» примитив.

7. Как проверить, что примитив выбран правильно? На что смотреть в профиле?

Инструменты: (1) pprof блокировок (block profile) и мьютексных ожиданий (mutex profile) с runtime.SetBlockProfileRate/SetMutexProfileFraction — показывают, где горутины ждут и на каком мьютексе; (2) -race — корректность; (3) бенчмарки с -benchmem и p99-метрики под нагрузкой — итоговое сравнение вариантов. Признаки неверного примитива: конкуренция на мьютексе видна в mutex profile (виновник — конкретный Lock), длина очереди горутин растёт, латентность «пилит» синхронно с GC или с ростом RPS. Ответ уровня senior: «выбор подтверждается профилем и метриками латентности, а не бенчмарком в вакууме».

Прокачать тему в самоучителе

Читается быстро, запоминается практикой. Эти курсы закрывают тему с запуском кода прямо на странице: