← Все статьи

Параллельность в Go: горутины, каналы и пул работников

Почему одного слова go мало: каналы, ограниченный пул задач и отмена работы, когда клиент уже отключился.

Параллельность в Go: горутины, каналы и пул работников
Содержание

Коротко

Разбор на Dev.to начинается с того, что в Go параллельную работу запускают одним ключевым словом, и на этом понимание обычно заканчивается. Автор разбирает, зачем это нужно на сетевых задачах, как горутины обмениваются данными через каналы, как буфер и оператор выбора не дают программе зависнуть в одном ожидании и как пакет context останавливает работу, которую уже никто не ждёт.

Что произошло

Пример, с которого он стартует, бытовой. Нужно опросить пять серверов. Последовательная программа ждёт ответ первого, потом второго, потом третьего. Пока один адрес молчит, остальные простаивают. Горутина позволяет отправить проверку и заняться следующей, не дожидаясь завершения предыдущей. Для сети, файлов, фоновых заданий и очередей вызовов это естественный режим: большую часть времени программа всё равно ждёт чужой ответ, а не считает.

Канал в этом разборе — не украшение, а способ забрать результат, не залезая в чужую память руками. Буфер меняет ритм. Канал make(chan int, 10) держит десять значений, и отправитель блокируется только когда буфер полон. Это как раз случай, когда производитель и потребитель не совпадают по скорости: короткая пауза на приёме не останавливает всю отдачу. Оператор select ждёт несколько операций сразу — например, результат или сигнал таймаута, — и программа может ответить отказом, а не висеть на одном канале.

Пул работников ограничивает, сколько задач идёт одновременно. Без такого предела легко запустить горутину на каждый входящий запрос и упереться в память, файлы и соединения, хотя каждая отдельная горутина и кажется дешёвой. Автор ставит пул рядом с синхронизацией как практический приём, а не как необязательное усложнение: параллельность здесь про контроль ресурсов, а не про максимальное число одновременно живых задач.

Отдельный кусок — отмена. Запрос по HTTP стартовал поход в базу, пользователь закрыл соединение. Без явной отмены серверная работа продолжается вхолостую. Пакет context как раз передаёт срок и сигнал остановки вниз по вызовам, чтобы база и прочие ожидания завершились вместе с запросом, а не жили своей жизнью после ухода клиента.

Почему это важно

Первый день с Go создаёт иллюзию, что параллельность — это префикс перед вызовом функции. На нагрузке из многих ожиданий сети эта иллюзия дорого стоит: либо программа по-прежнему работает по очереди и простаивает, либо горутин становится столько, сколько запросов, и узким местом становятся соединения, а не язык. Канал, буфер, выбор по нескольким каналам и ограниченный пул — это и есть минимальный набор, без которого ключевое слово go только размножает ожидания.

Отмена важна по той же причине, что и пул. Незавершённая работа после обрыва клиента съедает те же соединения и те же работники, которые нужны живым запросам. Для API, опросов и фоновых заданий это ближе к устойчивости сервиса, чем к «продвинутой» главе учебника.

На практике

Разбор учебный: в нём нет замера на вашем профиле нагрузки и нет готового каркаса сервиса. Его стоит использовать как проверку, что в коде есть все четыре части, а не только запуск горутины. Пять серверов в примере легко заменить на пачку исходящих вызовов в своём обработчике и увидеть, ждёте ли вы их по очереди.

Буфер на десять значений — иллюстрация, не рекомендуемый размер очереди. Размер буфера и число работников выбирают по тому, сколько одновременных соединений вы реально можете держать.

  1. Разделите работу, которая в основном ждёт сеть или диск, и не гоняйте её строго по очереди, если ответы друг от друга не зависят.
  2. Забирайте результаты каналом, а не общей переменной без согласования.
  3. Ограничьте число одновременных задач пулом, если входной поток может быть больше, чем исходящие соединения.
  4. На несколько исходов — ответ, таймаут, отмена — используйте select, а не одно вечное чтение из канала.
  5. Прокидывайте context из входящего запроса в базу и во внешние вызовы, чтобы обрыв клиента останавливал работу.

Итог

Текст напоминает простую развилку: Go делает запуск параллельной работы дешёвым, но не делает её безопасной сам по себе. Каналы передают данные, буфер и select управляют ожиданием, пул держит потолок, context гасит лишнее. Для сервиса на входящих запросах этого достаточно, чтобы перестать путать одно ключевое слово с готовностью к нагрузке.

Комментарии

Загрузка комментариев…