Перевод публикуется с сокращениями, автор оригинальной статьи Ahad Hasan.
В некоторых языках программирования имеются мощные конструкции, которые могут выгружать работу в разные потоки ОС (например, Java), а другие только имитируют это поведение в одном потоке (например, Ruby).
У Golang есть мощная модель конкурентности – CSP (communicating sequential processes), которая разбивает проблему на более мелкие последовательные процессы, а затем планирует несколько экземпляров этих процессов, называемых Goroutines (горутины). Связь между горутинами осуществляется путем передачи неизменяемых сообщений через Channels.
Рассмотрим, как можно воспользоваться преимуществами конкурентности в Golang и как ограничить его использование с рабочими пулами.
Простой пример
Представим, что у нас есть внешний вызов API, выполняющийся около 100 мс. Если будет 1000 таких вызовов, и мы вызовем их синхронно, то для завершения потребуется около 100 секунд.
Здесь у нас простая модель, содержащая структуру данных только с целочисленными значениями. Массив данных мы обрабатываем массив синхронно: очевидно, что такое решение неоптимально решение, поскольку задачи можно выполнить одновременно. Давайте превратим это в асинхронный процесс с Goroutines и Channels.
Асинхронность
Здесь мы создаем буферизованный канал 100 и добавляем в него все данные, переданные NoPooledWork. Поскольку канал буферизованный, нельзя ввести больше 100 экземпляров данных до полного извлечения из него, что и происходит внутри горутины. Мы перемещаемся по каналу, извлекаем из него данные, добавляем горутину и обрабатываем. Здесь нет ограничений на количество созданных горутин, как нет и ограничений на обработку задач (следует учитывать необходимые ресурсы) – сколько пришло, столько обработали. Если мы запустим такой код, то выполним 1000 задач примерно за 100 мс.
Проблема
Если у нас нет безграничных ресурсов, их нужно ограниченно распределять в течение определенного периода времени. Минимальный размер объекта Goroutine составляет 2 К, но он может достигать 1 ГБ. Приведенное выше решение выполняет все задачи параллельно, а для миллиона таких задач оно может быстро исчерпать память и ресурсы процессора. Придется либо модернизировать машину, либо найти другой подход.
Существует блестящее решение под названием Thread Pool или Worker Pool. Идея состоит в том, чтобы иметь ограниченный пул worker-ов для обработки задач. Как только "рабочий" закончит с одной из них, он переходит к следующей. Это уменьшает нагрузку на процессор и память, а также оперативнее распределяет задачи с течением времени.
Решение: Worker Pool
Исправим описанную проблему и реализуем рабочий пул:
Здесь у нас есть ограниченное количество worker-ов (100) и ровно 100 горутин для обработки задач. Каналы можно рассматривать как очереди, а каждый worker – как клиента. Несколько горутин могут прослушивать один и тот же канал, но каждый элемент в нем будет обработан только один раз.
Это хорошее решение, и если мы запустим его, то увидим, что для завершения всех задач требуется 1 секунда. Не совсем 100 мс, но нам это и не нужно. Мы получаем гораздо лучшее решение, которое распределяет нагрузку во времени.
Обработка ошибок
Код уже выглядит как готовый продукт, но это не так, поскольку мы не обрабатываем ошибки. Давайте создадим сценарий, в котором посмотрим, как можно это реализовать:
Мы модифицировали метод обработки некоторых ошибок и добавили его в канал переданных ошибок. Таким образом для обработки ошибок в параллельной модели нам нужен канал для хранения данных о них. После того, как все задачи завершены, мы его проверяем. Объект error содержит идентификатор задачи, так что при необходимости мы можем обработать их снова.
Это лучшее решение, чем то, которое вообще не учитывало ошибки. Во второй части туториала рассмотрим, как сделать выделенный и надежный пакет рабочих пулов, который сможет обрабатывать параллельные задачи с ограниченным количеством Worker pool.
Заключение
Мы рассмотрели синхронный и асинхронный подходы, обработку ошибок, горутины и функционирование worker-ов. Модель конкурентности Golang достаточно мощна, чтобы просто построить решение Worker pool без особых накладных расходов, поэтому она не включена в стандартную библиотеку. Однако мы всегда можем создать собственное решение, которое соответствует нашим потребностям. Скоро будет следующая статья, а пока следите за обновлениями.
Дополнительные материалы:
Комментарии