Паттерны синхронизации в Go
Числа с одной машины, два ядра. Инкремент счётчика: atomic 9,6 наносекунды, мьютекс 15,4, обмен через канал 47,6. Канал медленнее мьютекса втрое, а atomic - впятеро. Спрашивают, чем руководствоваться при выборе, что даёт errgroup и когда уместна sync.Map. Проверяют инженерное суждение, а не знание сигнатур.
Стержень: канал - когда данные передаются и владение переходит; мьютекс - когда состояние общее и остаётся на месте.
// Формулировки: «канал или мьютекс?», «зачем errgroup?», «когда sync.Map?»
Передача против защиты
Канал уместен там, где есть поток данных и владение переходит от горутины к горутине: конвейер, очередь заданий, сбор результатов от воркеров. Отправил значение - оно больше не твоё, и блокировки вокруг него не нужны.
Мьютекс уместен, когда состояние общее и остаётся на месте: кэш, счётчики, конфигурация, к которой ходят все. Заворачивать такое в канал значит заводить лишнюю горутину-владельца ради того, что решается тремя строками.
// Цена написана выше: 47,6 наносекунды против 15,4. Каналы выбирают за ясность потока данных, а не за скорость - на голом счётчике они проигрывают втрое.
- передача владения
- после отправки значение принадлежит получателю
- общее состояние
- данные, к которым обращаются многие горутины
sync.Map: когда выигрыш есть и какой
Замерил оба профиля на 64 ключах. Только чтение: sync.Map 18,1 нс, обычная мапа под RWMutex 20,2 - выигрыш десять процентов, на двух ядрах почти незаметный. Смешанная нагрузка, каждая десятая операция на запись: sync.Map 51,8 против 25,7. Проигрыш ровно вдвое.
Отсюда и формулировка её профиля: ключ записали один раз и дальше только читают, либо горутины работают с непересекающимися ключами. Всё остальное лучше отдать обычной мапе под RWMutex, где заодно сохраняется типизация - у sync.Map каждое чтение стоит приведения типа и проверки.
// Оговорка честности: выигрыш sync.Map растёт с числом ядер, а у меня их два. Направление это не меняет - на записях она проигрывает при любом количестве.
- sync.Map
- мапа под профиль «записали раз, читают многие»
- приведение типа
- цена интерфейсного хранения на каждом чтении
errgroup и потолок параллелизма
errgroup.WithContext возвращает группу и производный контекст. Первая ошибка запоминается, контекст отменяется, задачи, которые его слушают, сворачиваются сами, а Wait отдаёт ту самую первую ошибку.
SetLimit ставит потолок одновременно работающих задач, и это не про элегантность. Десять тысяч горутин, разом пошедших в базу с пулом на двадцать соединений, положат не твой сервис, а базу - и вместе с ней соседей, которые ей пользуются.
// И снова кооперативность: отмена сработает только у задач, которые слушают ctx. Горутина, игнорирующая контекст, доработает до конца, сколько бы ошибок ни случилось у соседей.
- errgroup
- группа задач: ожидание, первая ошибка, отмена
- семафор
- ограничение числа одновременно работающих задач
Как отвечать: «Канал или мьютекс - как выбираете?»
Смотрю на природу задачи. Если есть поток данных и владение переходит от одной горутины к другой - конвейер, очередь заданий, сбор результатов от воркеров, беру каналы: отправил значение, дальше оно не моё, блокировки вокруг не нужны. Если состояние общее и остаётся на месте - кэш, счётчики, конфигурация, к которой ходят все, беру мьютекс: заворачивать это в канал значит заводить лишнюю горутину-владельца ради трёх строк. Про производительность помню, что каналы не бесплатны, я мерил: обмен через канал 47,6 наносекунды против 15,4 у мьютекса и 9,6 у atomic. То есть каналы выбирают за ясность потока данных, а не за скорость. Когда задачи возвращают ошибки, беру errgroup - он собирает первую ошибку, отменяет общий контекст, а через SetLimit ограничивает параллелизм, чтобы десять тысяч горутин не пришли разом в базу с пулом на двадцать соединений. Специализированные штуки вроде sync.Map беру только после профиля: на моём замере она выиграла у мапы под RWMutex десять процентов на чистом чтении и проиграла вдвое, как только каждая десятая операция стала записью.
Сильный ответ: критерий сформулирован через передачу владения против защиты состояния, цена каналов названа честно и числом, errgroup подан вместе с эксплуатационной заботой об ограничении параллелизма, а профиль sync.Map подкреплён замером в обе стороны.
На чём валят
- −Заворачивают простой счётчик в горутину с каналом вместо мьютекса.
- −Считают каналы всегда быстрее блокировок. На счётчике канал проиграл мьютексу втрое.
- −Берут sync.Map по умолчанию. На смешанной нагрузке она проиграла вдвое.
- −Запускают тысячи задач без потолка параллелизма и роняют внешний сервис или базу.
- −Ждут, что errgroup остановит задачи, которые не слушают контекст.
Проверьте себя
Пять вопросов из банка по этой подтеме. Всего их 4, остальные разбираются в тренажёре.
- Структура со встроенным sync.Mutex передана в метод с получателем-значением func (s S). В чём риск?A)Копируется и мьютекс — Lock идёт по разным копиям, защита не работаетB)Встроенный мьютекс требует, чтобы структуру передавали через указательC)Компилятор отклонит код: копировать sync.Mutex запрещеноD)Ничего: Go передаёт встроенный мьютекс по ссылке, даже если получатель — значение
показать ответ и разбор
+A)Копируется и мьютекс — Lock идёт по разным копиям, защита не работает// разбор: Получатель-значение копирует всю структуру, включая Mutex. Копия мьютекса — независимый замок с обнулённым состоянием, поэтому горутины блокируются на разных копиях и синхронизации нет (а копирование залоченного и вовсе даёт битое состояние). go vet это ловит: «passes lock by value». Правило: методы со встроенным мьютексом — на получателе-указателе. Спот-чек подтвердил варнинг.
- Как в Go выбирают между каналом и мьютексом для общего состояния?A)Канал предпочтительнее в новом коде — мьютексы считаются наследиемB)Мьютекс быстрее и проще, а каналы нужны лишь для сетевого кодаC)Канал — передать владение данными; мьютекс — защитить состояние на местеD)Разницы нет: компилятор превращает каналы в мьютексы под капотом внутри рантайма
показать ответ и разбор
+C)Канал — передать владение данными; мьютекс — защитить состояние на месте// разбор: Девиз Go «share memory by communicating»: если данные логически ПЕРЕДАЮТСЯ от горутины к горутине (пайплайн, воркеры, передача владения) — канал выражает это чище. Если несколько горутин работают с ОБЩИМ состоянием на месте (кэш, счётчик, карта) — проще и дешевле мьютекс/atomic. Оба инструмента легитимны, выбор по смыслу задачи, а не по моде.
- Что даёт errgroup поверх обычной WaitGroup?A)Ограничивает число горутин, но ошибки по-прежнему собирает вручнуюB)Собирает первую ошибку и отменяет общий контекст остальных задачC)Завершает горутины в порядке их запускаD)Перезапускает упавшие задачи заданное число раз
показать ответ и разбор
+B)Собирает первую ошибку и отменяет общий контекст остальных задач// разбор: WaitGroup умеет только «дождаться всех», и ошибки приходится тянуть через отдельный канал руками. errgroup.WithContext возвращает группу и производный контекст: первая ошибка запоминается, контекст отменяется, и остальные задачи, которые его слушают, сворачиваются сами. Wait отдаёт эту первую ошибку. Есть и ограничение параллелизма через SetLimit.
- Когда sync.Map выигрывает у обычной map под мьютексом?A)Она быстрее обычной map с мьютексом на смешанной нагрузке чтений и записейB)Когда ключи пишутся однократно, а читаются многими горутинамиC)Когда нужен обход всех ключей в предсказуемом порядкеD)Когда карта хранит больше нескольких тысяч ключей
показать ответ и разбор
+B)Когда ключи пишутся однократно, а читаются многими горутинами// разбор: sync.Map заточена под два узких профиля: ключ записали один раз и потом только читают, либо разные горутины работают с непересекающимися ключами. Там она избегает общей блокировки. В смешанной нагрузке с активными записями она обычно проигрывает обычной map под RWMutex, а ещё теряет типизацию: значения приходят как any и требуют приведения.
дальше
Теорию прочитали. Навык ставится повторением
В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.