Мьютексы и примитивы синхронизации
Mutex и RWMutex, WaitGroup, Once и потокобезопасный доступ к map.
13 вопросов
JuniorТеорияОчень частоЧто предоставляет sync.Mutex и как им правильно пользоваться?
Что предоставляет sync.Mutex и как им правильно пользоваться?
sync.Mutex даёт взаимное исключение: только одна goroutine удерживает блокировку одновременно, защищая критическую секцию. Вызовите Lock до секции и Unlock после — обычно через defer. Нулевое значение — это разблокированный, готовый к работе мьютекс.
Типичные ошибки
- ✗Забыть Unlock на пути раннего return — ставьте defer Unlock сразу после Lock
- ✗Копировать структуру со встроенным sync.Mutex после первого использования — копируется состояние блокировки
- ✗Считать Mutex реентерабельным — goroutine, повторно вызвавшая Lock на своём мьютексе, входит в deadlock
Уточняющие вопросы
- →Почему sync.Mutex нельзя копировать после первого использования?
- →Что произойдёт, если Unlock вызовет goroutine, которая не делала Lock?
JuniorТеорияОчень частоДля чего нужен sync.WaitGroup и каков контракт Add/Done/Wait?
Для чего нужен sync.WaitGroup и каков контракт Add/Done/Wait?
sync.WaitGroup ждёт завершения набора goroutine. Add(n) повышает внутренний счётчик, каждая goroutine вызывает Done, уменьшая его, а Wait блокируется, пока счётчик не достигнет нуля. Add должен выполниться до запуска goroutine, которую он учитывает.
Типичные ошибки
- ✗Вызывать Add внутри goroutine, а не до неё — возникает гонка с Wait
- ✗Забыть defer wg.Done(), и Wait блокируется навсегда при раннем return goroutine
- ✗Передавать WaitGroup по значению в goroutine — Done обновляет копию
Уточняющие вопросы
- →Что произойдёт, если счётчик WaitGroup уйдёт ниже нуля?
- →Можно ли переиспользовать один WaitGroup для второй партии goroutine?
MiddleТеорияОчень частоКогда sync.RWMutex лучше sync.Mutex и в чём цена?
Когда sync.RWMutex лучше sync.Mutex и в чём цена?
sync.RWMutex допускает много одновременных читателей RLock ЛИБО одного писателя Lock. Он выигрывает на read-heavy нагрузке, где читателей намного больше писателей. Цена — более тяжёлая блокировка: медленнее sync.Mutex при низкой конкуренции, и ожидающий писатель блокирует новых читателей.
Типичные ошибки
- ✗Брать RWMutex по умолчанию — при низкой конкуренции он медленнее обычного Mutex
- ✗Считать, что писатели работают параллельно — Lock полностью эксклюзивен, как у Mutex
- ✗Думать, что читатели уморят писателя голодом — ожидающий писатель блокирует новых читателей
Уточняющие вопросы
- →Почему ожидающий писатель блокирует новых читателей, а не ждёт, пока они закончатся?
- →Как измерить, действительно ли RWMutex обгоняет Mutex на вашей нагрузке?
JuniorТеорияЧастоЧто предоставляет стандартный пакет sync?
Что предоставляет стандартный пакет sync?
sync предоставляет низкоуровневые примитивы конкурентности: Mutex и RWMutex для взаимного исключения, WaitGroup для ожидания группы горутин, Once для однократной инициализации, Cond для ожидания по условию, Map для конкурентной map и Pool для переиспользования временных объектов. Подпакет sync/atomic добавляет атомарные операции без блокировок.
Типичные ошибки
- ✗Думать, что в
syncсоздают горутины и каналы — это языковые встроенные средства - ✗Считать, что
sync.Mapвсегда быстрее обычной map подMutex - ✗Путать
sync.Pool(переиспользование временных объектов) с пулом соединений к БД
Уточняющие вопросы
- →Когда
sync.Mapдействительно быстрее, чем map подMutex? - →Для чего нужен
sync.Poolи когда его объекты освобождаются?
MiddleТеорияЧастоВ чём разница между состоянием гонки (data race) и deadlock и как предотвращать каждое?
В чём разница между состоянием гонки (data race) и deadlock и как предотвращать каждое?
Состояние гонки (data race) — это две goroutine, одновременно обращающиеся к одной памяти, где хотя бы одна пишет, без синхронизации, что даёт неопределённый результат; ловите его детектором -race и устраняйте через sync.Mutex, канал или atomic. Deadlock — это goroutine, каждая из которых ждёт ресурс, удерживаемый другой, так что ни одна не движется; предотвращайте его согласованным порядком взятия блокировок.
Типичные ошибки
- ✗Путать состояние гонки с deadlock — это разные сбои с разными способами устранения
- ✗Считать, что детектор
-raceловит deadlock; он находит лишь несинхронизированный доступ к памяти - ✗Несогласованный порядок взятия блокировок в разных goroutine — классический рецепт deadlock
Уточняющие вопросы
- →Почему детектор
-raceнаходит состояния гонки, но никогда не сообщает о deadlock? - →Как согласованный порядок взятия блокировок предотвращает deadlock между двумя мьютексами?
MiddleТеорияЧастоКак sync.Once гарантирует однократное выполнение?
Как sync.Once гарантирует однократное выполнение?
sync.Once.Do(f) выполняет f ровно один раз для всех вызывающих. Он использует атомарный флаг done для дешёвого быстрого пути и мьютекс для медленного: первый вызывающий выполняет f под блокировкой, конкурентные вызывающие ждут её завершения, а поздние вызовы видят флаг и сразу возвращаются.
Типичные ошибки
- ✗Думать, что Do возвращается до завершения f — конкурентные вызывающие ждут конца f
- ✗Считать Once свойством goroutine, а не общим для всех вызывающих одного значения
- ✗Считать, что хватает одного атомарного флага — для корректности медленный путь требует мьютекс
Уточняющие вопросы
- →Почему sync.Once нужны и атомарный флаг, и мьютекс, а не что-то одно?
- →Что произойдёт с другими вызывающими Do, если переданная в Do функция вызовет panic?
MiddleТеорияЧастоКак устроена sync.Map и когда она уместнее обычной map под мьютексом?
Как устроена sync.Map и когда она уместнее обычной map под мьютексом?
sync.Map держит две внутренние map — преимущественно читаемую read, обслуживаемую без блокировки через атомики, и dirty под Mutex для записи. Чтение уже присутствующих ключей вовсе обходится без блокировки. Она выигрывает только на read-mostly или write-once-read-many нагрузках; для обычного смешанного чтения/записи обычная map под RWMutex быстрее.
Типичные ошибки
- ✗Использовать sync.Map по умолчанию для пишущих map, где map под RWMutex быстрее
- ✗Считать sync.Map просто обычной map, обёрнутой в RWMutex
- ✗Полагать, что запись в sync.Map lock-free
Уточняющие вопросы
- →Почему продвижение
dirty-map может временно замедлить всплеск записей? - →Как со временем
read-map обновляется изdirty-map?
MiddleДебаггингИногдаПочему запись во встроенную map из многих горутин падает и как это исправить?
Почему запись во встроенную map из многих горутин падает и как это исправить?
Встроенные map не безопасны для конкурентного доступа. Конкурентные записи аварийно завершают программу с fatal error: concurrent map writes — даже чтение с записью одновременно может упасть. Исправление — защищать каждый доступ sync.Mutex/RWMutex либо использовать sync.Map под конкуренцией.
Типичные ошибки
- ✗Считать встроенную map безопасной для конкурентных записей, если ключи различны
- ✗Думать, что это поправимый data race, а не фатальная ошибка runtime
- ✗Считать, что преразмер map устраняет нужду в синхронизации
Уточняющие вопросы
- →Когда
sync.Mapпредпочтительнее обычной map подMutex? - →Почему даже одновременные чтение и запись могут упасть, а не только две записи?
MiddleДебаггингИногдаПочему вызов wg.Add(1) внутри каждой горутины — это ошибка и где он должен быть?
Почему вызов wg.Add(1) внутри каждой горутины — это ошибка и где он должен быть?
wg.Add(1) вызывается внутри горутины, но планировщик может не запустить ни одну горутину до того, как main дойдёт до wg.Wait(). Если Wait видит нулевой счётчик, он сразу возвращается, и программа может выйти до запуска горутин. Исправление — вызывать wg.Add(1) в цикле перед запуском каждой горутины.
Типичные ошибки
- ✗Вызывать
wg.Add(1)внутри горутины, а не перед её запуском - ✗Считать, что планировщик запустит горутины до того, как
mainдойдёт доwg.Wait() - ✗Думать, что ошибка в отложенном
Done, а не в запоздаломAdd
Уточняющие вопросы
- →Почему добавление в WaitGroup до
goустанавливает нужный happens-before сWait? - →Что документация
Add/Waitговорит о конкурентном вызовеAdd?
SeniorДебаггингИногдаРевью кода: высоконагруженный in-memory кэш на sync.Mutex — найдите ошибки конкуренции
Ревью кода: высоконагруженный in-memory кэш на sync.Mutex — найдите ошибки конкуренции
sync.Mutex — локальная переменная, поэтому каждый вызов блокирует свою копию, а общий cache ничем не защищён — конкурентные вызовы гоняются и падают с fatal error: concurrent map writes. Ещё value = cache[key] перезаписывает аргумент, поэтому путь создания сохраняет "", а снятие блокировки между чтением и записью делает get-or-create неатомарным. Исправление: один общий замок — RWMutex, раз чтений больше, — удерживаемый на всём check-then-set, и не затирать value.
Типичные ошибки
- ✗Не заметить, что мьютекс — локальная переменная, поэтому ничего не синхронизирует между горутинами
- ✗Упустить, что
value = cache[key]перезаписывает аргумент, и путь создания сохраняет пустую строку - ✗Считать get-or-create атомарным, хотя чтение и запись под разными блокировками
Уточняющие вопросы
- →Почему взятие
RLockна чтение и затемLockна запись всё равно позволяет двум вызывающим создать значение? - →Как
sync.Mapили группаsingleflightизменили бы этот дизайн?
SeniorДебаггингИногдаПочему этот код может уйти в deadlock, когда A держит RLock и вызывает B, берущий write-Lock?
Почему этот код может уйти в deadlock, когда A держит RLock и вызывает B, берущий write-Lock?
RWMutex в Go не реентрантен. A держит read-блокировку и вызывает B, который запрашивает write-Lock. Если другая горутина уже ждёт Lock, runtime блокирует новых читателей, чтобы избежать голодания писателя — поэтому B ждёт read-блокировку, которую всё ещё держит A. Документация запрещает рекурсивный RLock; берите блокировку один раз на верхнем уровне.
Типичные ошибки
- ✗Считать
sync.RWMutexреентрантным — рекурсивное взятие может привести к deadlock - ✗Думать, что deadlock безусловен, а не вызван конкурентно ждущим писателем
- ✗Брать блокировку во вложенном вызове вместо одного взятия на верху цепочки вызовов
Уточняющие вопросы
- →Почему
RWMutexблокирует новых читателей, как только писатель ждёт? - →Как перестроить
AиB, чтобы блокировку брал только один из них?
SeniorДебаггингИногдаПочему этот сервер курсов валют не компилируется и верна ли его дисциплина RWMutex?
Почему этот сервер курсов валют не компилируется и верна ли его дисциплина RWMutex?
В mu &sync.RWMutex{} пропущен := — нужно mu := &sync.RWMutex{}. В остальном дисциплина блокировок верна: updater переприсваивает rates под Lock, хендлеры читают под RLock, поэтому каждый доступ защищён. Проверку != http.ErrServerClosed на ListenAndServe оставьте.
Типичные ошибки
- ✗Считать, что пропущенный
:=— опечатка вместо=:muне объявлена, компилируется только:= - ✗Думать, что переприсваивание map под Lock гонится с RLock-читателями — блокировка сериализует оба
- ✗Считать чистое завершение
ListenAndServeошибкой — оно возвращаетhttp.ErrServerClosed
Уточняющие вопросы
- →Почему переприсваивание map
ratesподLockбезопасно, а мутация подRLock— нет? - →Что сломается, если фоновый updater подменял бы
ratesподRLockвместоLock?
SeniorКодИногдаПотокобезопасный in-memory TTL-кэш для User
Потокобезопасный in-memory TTL-кэш для User
Храните записи в map с ключом User.ID, у каждой — момент истечения, под защитой sync.RWMutex. Set пишет значение и now+ttl под Lock; Get берёт RLock и считает запись с истёкшим сроком промахом. Фоновая goroutine-чистильщик выметает истёкшие ключи, чтобы нетронутые записи не оставались; метод Close останавливает её, чтобы она не утекала.
Типичные ошибки
- ✗Возвращать устаревшую запись на
Get, потому что момент истечения не проверяется при чтении - ✗Читать или писать map без блокировки, считая map в Go безопасными при конкуренции
- ✗Никогда не удалять истёкшие ключи, из-за чего map растёт безгранично, хотя
Getсообщает промахи
Уточняющие вопросы
- →Как запускать чистильщик периодически, не утекая его goroutine после удаления кэша?
- →Почему здесь
RWMutexпредпочтительнееMutexи когда этот выбор перестаёт окупаться?