Конкурентность Go
Почему CAS не замечает проблему ABA?
CAS сравнивает только текущее значение с ожидаемым. Если goroutine прочитала A, другая успела заменить A на B и вернуть A, первый CAS увидит прежние биты и ошибочно решит, что состояние не менялось. Это ABA. Решение добавляет к значению monotonically changing version или tag и выполняет CAS над парой, либо использует mutex или алгоритм reclamation, который не позволяет повторно использовать объект слишком рано. Один указатель особенно опасен при удалении и повторном выделении памяти. Конкретная упаковка указателя и версии должна соблюдать требования атомарности и выравнивания платформы.
Ссылки для изучения
Когда в Go нужен sync.Cond?
sync.Cond нужен, когда несколько goroutines ждут изменения общего предиката под Locker, а событие само не является передаваемым значением. Проверка всегда идёт в цикле: lock, for !condition { cond.Wait() }, работа, unlock. Wait атомарно освобождает lock и после пробуждения захватывает его снова. Signal будит одну ожидающую goroutine, Broadcast все; пробуждение не гарантирует, что условие всё ещё истинно. Для очереди работ или одноразового уведомления channel обычно проще и безопаснее. Cond полезен для сложного общего состояния с несколькими возможными изменениями.
Ссылки для изучения
Когда errgroup удобнее WaitGroup?
errgroup.Group удобен для набора связанных goroutines, где caller ждёт их все и хочет получить первую ненулевую ошибку. errgroup.WithContext даёт context, который отменяется при первой ошибке или завершении Wait, поэтому остальные задачи могут остановить I/O и вычисления. SetLimit ограничивает число активных функций, TryGo позволяет не блокировать добавление. WaitGroup только считает завершения и не переносит ошибку или отмену, зато подходит, когда эти механизмы не нужны. Отмена errgroup остаётся кооперативной: функция должна использовать полученный context, иначе Wait продолжит ждать её.
Ссылки для изучения
Как избежать deadlock при блокировке двух mutex?
Все места, где нужны два mutex, должны захватывать их в одном глобальном порядке, например по неизменяемому ID объекта, и освобождать в обратном. Критические секции делают короткими и не выполняют внутри неизвестный callback, сеть или channel send. Если порядок трудно доказать, модель меняют: один mutex защищает общий инвариант, данные принадлежат одной goroutine либо операция разбивается без одновременных locks. TryLock с retry часто превращает deadlock в livelock и требует обоснования. Диагностика опирается на goroutine dump, block profile и точное владение locks.
Ссылки для изучения
Как race detector находит data race в Go?
Race detector инструментирует обращения к памяти и синхронизацию, затем во время выполнения ищет конфликтующие доступы из разных goroutines, где хотя бы один является записью и нет установленного happens-before. Запускают go test -race ./..., а также реальные сценарии через собранный с -race бинарник. Он видит только выполненные пути, поэтому успешный прогон не доказывает отсутствие races. Инструментация заметно увеличивает время и память. Отчёт содержит стеки конфликтующих доступов и создания goroutines; исправляют общее владение или синхронизацию, а не просто скрывают участок.
Ссылки для изучения
Чем data race отличается от race condition?
Data race в модели Go: два конкурентных доступа к одной области памяти, хотя бы один из них запись, без требуемой синхронизации. Это нарушение memory model, которое находит -race. Race condition шире: результат зависит от порядка событий, даже если каждый доступ синхронизирован. Например, две операции могут по отдельности безопасно проверить баланс и затем списать средства в неправильной последовательности под разными lock-секциями. Data race часто создаёт race condition, но множества не совпадают. Первую устраняют синхронизацией доступа; вторую, атомарностью бизнес-операции, протоколом или явной моделью состояния.
Ссылки для изучения
Почему RWMutex может быть медленнее Mutex?
RWMutex добавляет учёт читателей и более сложную координацию. Если критические секции короткие, конкуренция мала, writes регулярны или protected data плохо помещается в cache, этот overhead может быть выше выигрыша от параллельных reads. Долгий reader задерживает writer, а ожидающий writer влияет на новых readers по правилам реализации. Поэтому «чтений больше» недостаточно. Сначала используют простой Mutex, затем benchmark реального соотношения операций, длительности секций и числа goroutines. Иногда copy-on-write, atomic snapshot или разделение данных дают лучшее масштабирование, но усложнение оправдывают профилем.
Ссылки для изучения
Чем livelock отличается от deadlock?
При deadlock goroutines заблокированы и не могут сделать следующий шаг. При livelock они активны и постоянно реагируют друг на друга, но полезный прогресс отсутствует. Пример: два worker одновременно уступают один ресурс, повторяют проверку в одинаковом ритме и снова сталкиваются. CPU и логи могут быть активны, что отличает симптом от deadlock. Исправляют протокол владения, вводят ограниченное число попыток, случайный jitter или централизованную координацию. Один backoff без асимметрии может лишь замедлить livelock. Метрика должна отслеживать завершённую работу, а не число итераций цикла.
Ссылки для изучения
Примеры хороших ответов из реальных собеседований
- Разбор Go-собеседования: конкурентность и споры с интервьюером · 20:18–20:42Разбор интервью · Ответ кандидата
Кандидат отличает livelock от deadlock по продолжающейся бесполезной работе и расходу процессорного времени без прогресса.
Что такое starvation в конкурентной программе?
Starvation означает, что конкретная goroutine долго не получает CPU, lock, очередь или другой ресурс, хотя система в целом продолжает работать. Причины: длинная критическая секция, постоянный поток конкурентов, приоритетная очередь без aging или busy loop. Симптомы видны как большой tail latency и редкие зависшие задачи при нормальном throughput. Уменьшают время удержания lock, ограничивают конкуренцию, делают очереди справедливее и добавляют backpressure. Нельзя полагаться на недокументированную fairness mutex или scheduler. Прогресс проверяют per-request метриками и стресс-тестом, а не только отсутствием deadlock.
Ссылки для изучения
Зачем ограничивать параллелизм через worker pool?
Worker pool ограничивает число одновременно выполняемых дорогих операций и тем самым защищает CPU, память, соединения и downstream. Producer отправляет задачи в bounded channel, фиксированное число workers читает их и возвращает результаты или ошибки. Заполненная очередь создаёт backpressure; политика должна явно решить, ждать, отклонять или сбрасывать задачу. Закрывает channel только producer, workers завершают range, а context останавливает приём и I/O. Ошибки удобно собирать через errgroup, но cancellation должна быть кооперативной. Размер pool выбирают измерением ресурса, а не числом goroutines «на глаз».
Ссылки для изучения
Примеры хороших ответов из реальных собеседований
- Мок-собеседование на Middle Go-разработчика в Wildberries с код-ревью · 49:32–51:42Мок-собеседование · Ответ кандидата
Кандидат предлагает фиксированное число goroutine, которые берут задачи из канала, чтобы ограничить одновременную запись на диск и нагрузку на ОС.







