сеньорчикОткрыть в Telegram
← вся теориятеория к собесу · Горутины и каналы

Паттерны синхронизации в 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, остальные разбираются в тренажёре.

  1. #go_sync_patterns1 / 4
    Структура со встроенным sync.Mutex передана в метод с получателем-значением func (s S). В чём риск?
    A)Копируется и мьютекс — Lock идёт по разным копиям, защита не работает
    B)Встроенный мьютекс требует, чтобы структуру передавали через указатель
    C)Компилятор отклонит код: копировать sync.Mutex запрещено
    D)Ничего: Go передаёт встроенный мьютекс по ссылке, даже если получатель — значение
    показать ответ и разбор
    +A)Копируется и мьютекс — Lock идёт по разным копиям, защита не работает

    // разбор: Получатель-значение копирует всю структуру, включая Mutex. Копия мьютекса — независимый замок с обнулённым состоянием, поэтому горутины блокируются на разных копиях и синхронизации нет (а копирование залоченного и вовсе даёт битое состояние). go vet это ловит: «passes lock by value». Правило: методы со встроенным мьютексом — на получателе-указателе. Спот-чек подтвердил варнинг.

  2. #go_sync_patterns2 / 4
    Как в Go выбирают между каналом и мьютексом для общего состояния?
    A)Канал предпочтительнее в новом коде — мьютексы считаются наследием
    B)Мьютекс быстрее и проще, а каналы нужны лишь для сетевого кода
    C)Канал — передать владение данными; мьютекс — защитить состояние на месте
    D)Разницы нет: компилятор превращает каналы в мьютексы под капотом внутри рантайма
    показать ответ и разбор
    +C)Канал — передать владение данными; мьютекс — защитить состояние на месте

    // разбор: Девиз Go «share memory by communicating»: если данные логически ПЕРЕДАЮТСЯ от горутины к горутине (пайплайн, воркеры, передача владения) — канал выражает это чище. Если несколько горутин работают с ОБЩИМ состоянием на месте (кэш, счётчик, карта) — проще и дешевле мьютекс/atomic. Оба инструмента легитимны, выбор по смыслу задачи, а не по моде.

  3. #go_sync_patterns3 / 4
    Что даёт errgroup поверх обычной WaitGroup?
    A)Ограничивает число горутин, но ошибки по-прежнему собирает вручную
    B)Собирает первую ошибку и отменяет общий контекст остальных задач
    C)Завершает горутины в порядке их запуска
    D)Перезапускает упавшие задачи заданное число раз
    показать ответ и разбор
    +B)Собирает первую ошибку и отменяет общий контекст остальных задач

    // разбор: WaitGroup умеет только «дождаться всех», и ошибки приходится тянуть через отдельный канал руками. errgroup.WithContext возвращает группу и производный контекст: первая ошибка запоминается, контекст отменяется, и остальные задачи, которые его слушают, сворачиваются сами. Wait отдаёт эту первую ошибку. Есть и ограничение параллелизма через SetLimit.

  4. #go_sync_patterns4 / 4
    Когда sync.Map выигрывает у обычной map под мьютексом?
    A)Она быстрее обычной map с мьютексом на смешанной нагрузке чтений и записей
    B)Когда ключи пишутся однократно, а читаются многими горутинами
    C)Когда нужен обход всех ключей в предсказуемом порядке
    D)Когда карта хранит больше нескольких тысяч ключей
    показать ответ и разбор
    +B)Когда ключи пишутся однократно, а читаются многими горутинами

    // разбор: sync.Map заточена под два узких профиля: ключ записали один раз и потом только читают, либо разные горутины работают с непересекающимися ключами. Там она избегает общей блокировки. В смешанной нагрузке с активными записями она обычно проигрывает обычной map под RWMutex, а ещё теряет типизацию: значения приходят как any и требуют приведения.

дальше

Теорию прочитали. Навык ставится повторением

В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.