---
title: Конкурентность Go
questionDates:
  q-transfer-0472: '2026-10-01'
  q-transfer-0473: '2026-10-01'
  q-transfer-0474: '2026-10-01'
  q-transfer-0475: '2026-10-01'
  q-transfer-0476: '2026-10-01'
  q-transfer-0477: '2026-10-01'
  q-transfer-0478: '2026-10-01'
  q-transfer-0486: '2026-10-01'
  q-transfer-0487: '2026-10-01'
  q-transfer-0488: '2026-10-01'
seo:
  description: >-
    Тема «Конкурентность Go» для собеседования Go Developer. Почему CAS не
    замечает проблему ABA? Когда в Go нужен sync.Cond?
  title: Конкурентность Go — Go Developer
---

[Все темы Go Developer](/prep/go-developer)

## Почему CAS не замечает проблему ABA? [#q-transfer-0472]

CAS сравнивает только текущее значение с ожидаемым. Если goroutine прочитала `A`, другая успела заменить `A` на `B` и вернуть `A`, первый CAS увидит прежние биты и ошибочно решит, что состояние не менялось. Это ABA. Решение добавляет к значению monotonically changing version или tag и выполняет CAS над парой, либо использует mutex или алгоритм reclamation, который не позволяет повторно использовать объект слишком рано. Один указатель особенно опасен при удалении и повторном выделении памяти. Конкретная упаковка указателя и версии должна соблюдать требования атомарности и выравнивания платформы.

:::note[Ссылки для изучения]

1. [Почему CAS не замечает ABA: пример lock-free стека на C++](https://habr.com/ru/articles/195948/)
   :::

---

## Когда в Go нужен sync.Cond? [#q-transfer-0473]

`sync.Cond` нужен, когда несколько goroutines ждут изменения общего предиката под `Locker`, а событие само не является передаваемым значением. Проверка всегда идёт в цикле: lock, `for !condition { cond.Wait() }`, работа, unlock. `Wait` атомарно освобождает lock и после пробуждения захватывает его снова. `Signal` будит одну ожидающую goroutine, `Broadcast` все; пробуждение не гарантирует, что условие всё ещё истинно. Для очереди работ или одноразового уведомления channel обычно проще и безопаснее. `Cond` полезен для сложного общего состояния с несколькими возможными изменениями.

:::note[Ссылки для изучения]

1. [Условные переменные Cond: Wait, Signal и Broadcast — документация sync на русском](https://spec-zone.ru/go/sync/index)
   :::

---

## Когда errgroup удобнее WaitGroup? [#q-transfer-0474]

`errgroup.Group` удобен для набора связанных goroutines, где caller ждёт их все и хочет получить первую ненулевую ошибку. `errgroup.WithContext` даёт context, который отменяется при первой ошибке или завершении `Wait`, поэтому остальные задачи могут остановить I/O и вычисления. `SetLimit` ограничивает число активных функций, `TryGo` позволяет не блокировать добавление. `WaitGroup` только считает завершения и не переносит ошибку или отмену, зато подходит, когда эти механизмы не нужны. Отмена errgroup остаётся кооперативной: функция должна использовать полученный context, иначе `Wait` продолжит ждать её.

:::note[Ссылки для изучения]

1. [Go, Wait, SetLimit и TryGo пакета errgroup — раздел об errgroup](https://habr.com/ru/articles/871394/)
1. [Зачем передавать контекст errgroup каждой задаче — ошибка 4](https://habr.com/ru/articles/1085788/)
   :::

---

## Как избежать deadlock при блокировке двух mutex? [#q-transfer-0475]

Все места, где нужны два mutex, должны захватывать их в одном глобальном порядке, например по неизменяемому ID объекта, и освобождать в обратном. Критические секции делают короткими и не выполняют внутри неизвестный callback, сеть или channel send. Если порядок трудно доказать, модель меняют: один mutex защищает общий инвариант, данные принадлежат одной goroutine либо операция разбивается без одновременных locks. `TryLock` с retry часто превращает deadlock в livelock и требует обоснования. Диагностика опирается на goroutine dump, block profile и точное владение locks.

:::note[Ссылки для изучения]

1. [Две блокировки и deadlock: почему всем потокам нужен один порядок захвата](https://habr.com/ru/companies/ibm/articles/134698/)
   :::

---

## Как race detector находит data race в Go? [#q-transfer-0476]

Race detector инструментирует обращения к памяти и синхронизацию, затем во время выполнения ищет конфликтующие доступы из разных goroutines, где хотя бы один является записью и нет установленного happens-before. Запускают `go test -race ./...`, а также реальные сценарии через собранный с `-race` бинарник. Он видит только выполненные пути, поэтому успешный прогон не доказывает отсутствие races. Инструментация заметно увеличивает время и память. Отчёт содержит стеки конфликтующих доступов и создания goroutines; исправляют общее владение или синхронизацию, а не просто скрывают участок.

:::note[Ссылки для изучения]

1. [Как работает детектор гонок Go: инструментирование и выполненные пути](https://golang-blog.blogspot.com/2020/01/race-conditions-golang.html)
1. [Детектор гонок: примеры синхронизации и требования — руководство Go на русском](https://goru.dev/doc/articles/race_detector)
   :::

---

## Чем data race отличается от race condition? [#q-transfer-0477]

Data race в модели Go: два конкурентных доступа к одной области памяти, хотя бы один из них запись, без требуемой синхронизации. Это нарушение memory model, которое находит `-race`. Race condition шире: результат зависит от порядка событий, даже если каждый доступ синхронизирован. Например, две операции могут по отдельности безопасно проверить баланс и затем списать средства в неправильной последовательности под разными lock-секциями. Data race часто создаёт race condition, но множества не совпадают. Первую устраняют синхронизацией доступа; вторую, атомарностью бизнес-операции, протоколом или явной моделью состояния.

:::note[Ссылки для изучения]

1. [Data race и гарантии модели памяти — документация Go на русском](https://goru.dev/ref/mem)
1. [Различие data race и race condition — раздел определений доклада](https://habr.com/ru/companies/oleg-bunin/articles/1014080/)
   :::

---

## Почему RWMutex может быть медленнее Mutex? [#q-transfer-0478]

`RWMutex` добавляет учёт читателей и более сложную координацию. Если критические секции короткие, конкуренция мала, writes регулярны или protected data плохо помещается в cache, этот overhead может быть выше выигрыша от параллельных reads. Долгий reader задерживает writer, а ожидающий writer влияет на новых readers по правилам реализации. Поэтому «чтений больше» недостаточно. Сначала используют простой `Mutex`, затем benchmark реального соотношения операций, длительности секций и числа goroutines. Иногда copy-on-write, atomic snapshot или разделение данных дают лучшее масштабирование, но усложнение оправдывают профилем.

:::note[Ссылки для изучения]

1. [Устройство Mutex и RWMutex: CAS, счётчик читателей и ожидание writer](https://habr.com/ru/articles/744822/)
1. [Семантика ожидания читателей и writer в RWMutex — sync на русском](https://spec-zone.ru/go/sync/index)
   :::

---

## Чем livelock отличается от deadlock? [#q-transfer-0486]

При deadlock goroutines заблокированы и не могут сделать следующий шаг. При livelock они активны и постоянно реагируют друг на друга, но полезный прогресс отсутствует. Пример: два worker одновременно уступают один ресурс, повторяют проверку в одинаковом ритме и снова сталкиваются. CPU и логи могут быть активны, что отличает симптом от deadlock. Исправляют протокол владения, вводят ограниченное число попыток, случайный jitter или централизованную координацию. Один backoff без асимметрии может лишь замедлить livelock. Метрика должна отслеживать завершённую работу, а не число итераций цикла.

:::note[Ссылки для изучения]

1. [Deadlock и livelock: блокировка против активности без прогресса (на примере Java)](https://habr.com/ru/companies/otus/articles/427513/)
   :::

---

## Что такое starvation в конкурентной программе? [#q-transfer-0487]

Starvation означает, что конкретная goroutine долго не получает CPU, lock, очередь или другой ресурс, хотя система в целом продолжает работать. Причины: длинная критическая секция, постоянный поток конкурентов, приоритетная очередь без aging или busy loop. Симптомы видны как большой tail latency и редкие зависшие задачи при нормальном throughput. Уменьшают время удержания lock, ограничивают конкуренцию, делают очереди справедливее и добавляют backpressure. Нельзя полагаться на недокументированную fairness mutex или scheduler. Прогресс проверяют per-request метриками и стресс-тестом, а не только отсутствием deadlock.

:::note[Ссылки для изучения]

1. [Starvation и справедливость планирования: почему большой запрос может ждать бесконечно](https://habr.com/ru/companies/yandex/articles/431650/)
   :::

---

## Зачем ограничивать параллелизм через worker pool? [#q-transfer-0488]

Worker pool ограничивает число одновременно выполняемых дорогих операций и тем самым защищает CPU, память, соединения и downstream. Producer отправляет задачи в bounded channel, фиксированное число workers читает их и возвращает результаты или ошибки. Заполненная очередь создаёт backpressure; политика должна явно решить, ждать, отклонять или сбрасывать задачу. Закрывает channel только producer, workers завершают `range`, а context останавливает приём и I/O. Ошибки удобно собирать через `errgroup`, но cancellation должна быть кооперативной. Размер pool выбирают измерением ресурса, а не числом goroutines «на глаз».

:::note[Ссылки для изучения]

1. [Worker pool в Go: фиксированное число обработчиков, очередь задач и результаты](https://gobyexample.ru/worker-pools.html)
   :::
