MBL
Go / avito / Повторение / Паттерны конкурентности, практика руками и три класса проблем
Go сложный

Паттерны конкурентности, практика руками и три класса проблем

concurrencygoroutines

Вторая часть конспекта по конкурентности — типовые паттерны построения конкурентного кода (worker pool, pipeline, fan-in/fan-out) с рабочим кодом на каждый, формат живой практики "напиши руками за 15-20 минут" (те же паттерны + балансировщик), и три класса проблем, которые в них легко допустить.

Паттерны: worker pool, pipeline, fan-in/fan-out

Worker pool — фиксированное число N горутин читают задачи из общего канала параллельно. Ограничивает степень параллелизма и переиспользует горутины вместо запуска новой на каждую задачу.

Внутри воркера нужны ДВА select (не один!) — один на получение задачи (с ctx.Done()), второй на отправку результата (тоже с ctx.Done()): без второго воркер зависнет навсегда на блокирующей записи результата, если читатель результатов уже ушёл из-за отмены. Частая ошибка: закрыть канал результатов сразу после запуска воркеров, а не после того, как ВСЕ они реально закончили (wg.Wait() в отдельной горутине перед close) — иначе паника send on closed channel у воркера, который ещё пишет.

на play.golang.org

Pipeline — цепочка стадий, каждая своя горутина (или пул), соединённых каналами: выход одной — вход следующей. Данные текут потоково, не дожидаясь обработки ВСЕХ элементов на предыдущей стадии — снижает пиковую память.

на play.golang.org

Fan-out — один источник задач раздаётся нескольким обработчикам (то же самое, что делает worker pool выше: несколько воркеров читают из одного канала jobs). Fan-in — обратное: результаты нескольких каналов сливаются в один выходной, обычно после fan-out, чтобы собрать параллельную работу обратно вместе.

на play.golang.org

Практика: worker pool, балансировщик, pipeline руками

Формат: чистый лист, 15–20 минут, без подсказок — ровно как на секции по Go в Avito/Ozon/Яндексе. Разница между «понимаю каналы» и «умею писать конкурентный код» проверяется именно так — worker pool и pipeline руками смотри выше, здесь третий типовой формат той же практики: балансировщик.

В балансировщике с round-robin+retry под давлением времени легко забыть про потокобезопасность индекса текущего backend'а (нужен Mutex или atomic-счётчик), и про то, что нужно возвращать итоговую ошибку, если ВСЕ backend'ы отказали, а не зависать/молчать.

на play.golang.org

В pipeline (см. код выше) — классическая ловушка: читать входной канал через for chunk := range chunks вместо select с ctx.Done() — range не видит отмену контекста и не разблокируется, пока канал не закроют.

Интервьюер почти всегда спрашивает ПОСЛЕ того, как код написан: «а что если один backend не ошибку вернёт, а просто зависнет навсегда?» — прямая проверка, обёрнут ли вызов backend в select с ctx.Done(), а не просто вызван напрямую.

Утечка горутин, deadlock, race condition — три разных класса проблем

На собеседовании важно называть точный термин, а не «что-то не так с горутинами» — это три РАЗНЫХ класса проблем, которые часто путают между собой.

Утечка горутины — горутина заблокирована навсегда (обычно на отправке в канал, который никто не читает). Утечки накапливаются, каждая держит стек и ресурсы, пока процесс не упадёт по OOM. Профилактика: у КАЖДОЙ горутины должен быть явный гарантированный способ завершиться.

Deadlock — две+ горутины взаимно ждут друг друга, ПОЛНАЯ остановка. Классическая причина — захват нескольких мьютексов в разном порядке разными горутинами (ABBA-deadlock). Go детектирует deadlock, только если ВСЕ горутины программы заблокированы одновременно (паника all goroutines are asleep) — частичный deadlock (часть горутин продолжает работать) рантайм не ловит.

Race condition — конкурентный доступ к памяти без синхронизации, где хотя бы одно обращение — запись. counter++ — это чтение+инкремент+запись, не одна атомарная операция; результат непредсказуем, но программа НЕ падает и НЕ виснет (в отличие от двух пунктов выше) — просто даёт неверный ответ.

Ключевой факт про инструменты: go run -race находит гонки данных, но НЕ находит deadlock — это разные классы проблем с разной диагностикой. Конкурентная запись в обычную (не sync.Map) map без синхронизации — не тихая гонка, а немедленная паника рантайма fatal error: concurrent map writes, в отличие от гонки на простом int-счётчике, где часть инкрементов просто молча теряется.

Самопроверка 0 / 4
Помню, зачем воркеру в worker pool нужны ДВА select, а не один
Могу чётко различить утечку горутины, deadlock и race condition — это три разных класса проблем
Знаю, что go run -race находит гонки, но НЕ находит deadlock
Помню, что конкурентная запись в обычную map — не тихая гонка, а немедленная паника fatal error
Как усвоено?