Эффективное использование памяти при параллельных операциях ввода-вывода в Python

https://code.kiwi.com/memory-efficiency-of-parallel-io-operations-in-python-6e7d6c51905d
  • Перевод
Существует два класса задач где нам может потребоваться параллельная обработка: операции ввода-вывода и задачи активно использующие ЦП, такие как обработка изображений. Python позволяет реализовать несколько подходов к параллельной обработке данных. Рассмотрим их применительно к операциям ввода-вывода.

До версии Python 3.5 было два способа реализации параллельной обработки операций ввода-вывода. Нативный метод — использование многопоточности, другой вариант — библиотеки типа Gevent, которые распараллеливают задачи в виде микро-потоков. Python 3.5 предоставил встроенную поддержку параллелизма с помощью asyncio. Мне было любопытно посмотреть, как каждый из них будет работать с точки зрения памяти. Результаты ниже.

Подготовка тестовой среды


Для тестирования я создал простой скрипт. Хотя в нем и не так много функций, он демонстрирует реальный сценарий использования. Скрипт скачивает с сайта цены на автобусные билеты за 100 дней и готовит их для обработки. Потребление памяти измерялось с помощью memory_profiler. Код доступен на Github.

Поехали!


Синхронная обработка


Я реализовал однопоточную версию скрипта, которая стала эталоном для остальных решений. Использование памяти было довольно стабильным на протяжении всего исполнения, и очевидным недостатком стало время выполнения. Без какого-либо параллелизма сценарий занял около 29 секунд.

image

ThreadPoolExecutor


Работа с многопоточностью реализована в стандартной библиотеке. Самый удобный API предоставляет ThreadPoolExecutor. Однако, использование потоков связано с некоторыми недостатками, один из них — значительное потребление памяти. С другой стороны, существенное увеличение скорости выполнения является причиной, по которой мы хотим использовать многопоточность. Время выполнения теста ~17 секунд. Это значительно меньше ~29 секунд при синхронном выполнении. Разница — это скорость операций ввода-вывода. В нашем случае задержки сети.

image

Gevent


Gevent — это альтернативный подход к параллелизму, он приносит корутины в код Python до версии 3.5. Под капотом у нас легкие псевдо-потоки «гринлеты» плюс несколько потоков для внутренних нужд. Общее потребление памяти сходно с мультипаточностью.

image

Asyncio


С версии Python 3.5 корутины доступны в модуле asyncio, который стал частью стандартной библиотеки. Чтобы воспользоваться преимуществами asyncio, я использовал aiohttp вместо requests. aiohttp — асинхронный эквивалент requests со схожей функциональностью и API.

Наличие соответствующих библиотек — основной вопрос, который надо прояснить перед началом разработки с asyncio, хотя наиболее популярные IO библиотеки  —  requests, redis, psycopg2 — имеют асинхронные аналоги.

image

С asyncio потребление памяти значительно меньше. Оно схоже с однопоточной версией скрипта без параллелизма.

Пора начинать использовать asyncio?


Параллелизм — очень эффективный путь ускорения приложений с большим количеством операций ввода-вывода. В моем случае это ~40% прироста производительности в сравнении с последовательной обработкой. Различия в скорости для рассмотренных способов реализации параллелизма незначительны.

ThreadPoolExecutor и Gevent — мощные инструменты, способные ускорить существующие приложения. Их основное преимущество состоит в том, что в большинстве случаев они требуют незначительных изменений кодовой базы. Если говорить об общей производительности, то лучший инструмент — asyncio. Его потребление памяти значительно ниже по сравнению с другими методами параллелизма, что не влияет на общую скорость. За плюсы приходится платить специализированными библиотеками, заточенными под работу с asyncio.
  • +23
  • 5,9k
  • 8
Поделиться публикацией
Похожие публикации
Ой, у вас баннер убежал!

Ну. И что?
Реклама
Комментарии 8
  • +1
    Чтобы воспользоваться преимуществами asyncio, я использовал aiohttp вместо requests. asyncio — асинхронный эквивалент requests со схожей функциональностью и API.


    Опечатка? Наверное aiohttp это эквивалент?
    • 0
      Спасибо, поправил.
    • 0
      А это специально для каждого запроса своя сессия открывается? Общая сессия — залог более быстрой работы.
      Кроме того надо учитывать особенности библиотек при коннекте к одному серверу. У вас 100 запросов — это как раз предел для aiohttp (при общей сессии).
      Также сервер может использовать ограничение одновременных коннектов. Если у вас, скажем, максимум 5 одновременных скачиваний, то, скорее всего, полноценной одновременности не получится. Для более чистого теста, может быть, использовать просто чтение из файла?
      Также интересно было бы сравнить с PyPy — может быть там свои особенности?
      • 0
        задачи активно использующие ЦП, такие как обработка изображений
        странно, что multiprocessing вообще не упомянут
        • 0
          Кстати да, но как там мерять память?
        • –1

          asyncio это не "параллельная обработка"

          • 0
            Ну если смотреть формально, то потоки тоже не параллельные в питоне (CPython и тп).
            • 0

              Это формальная параллельность или псевдо-параллелизм. Даже множество запущенных процессов с одноядерным процессором концептуально не отменяет параллелизм. А вот асинхронность (acyncio) никак не параллелизм. Дело в переводе, в оригинальной статье (да и в офф документации) acyncio описан как concurrency. А конкурентность и параллельность это разные вещи. Можно почитать: https://m.habrahabr.ru/company/piter/blog/274569

          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

          Самое читаемое