Простое параллельное выполнение кода с помощью concurrent.futures
Начиная с версии 3.3, в Python включен многообещающий модуль concurrent.futures, с элегантным менеджером контекста для параллельно выполняющихся задач. Благодаря простоте и совместимости интерфейса вы можете использовать выполнение задач в отдельных потоках или процессах, прилагая при этом минимальные усилия.
Для "тяжелых" задач вы можете использовать всю мощность вашего компьютера, задействовав все CPU. Самый простой способ распределить выполнение задачи параллельно по процессам это использование ProcessPoolExecutor. ProcessPoolExecutor создаст достаточное количество подпроцессов, и задействует свободные ресурсы CPU.
Пример кода, с использованием менеджера контекста:
with concurrent.futures.ProcessPoolExecutor() as executor: result = executor.map(function, iterable)
Использование map - это самый простой способ для параллельного выполнения задач. map - применяет функцию function к каждому элементу списка iterable. function - может быть комплексной, а iterable - списком или большим массивом данных. Например, для запуска функции func 10 раз, с некоторыми данными data, вы можете сделать так:
executor.map(func, [data] * 10)
Этот подход один из самых простых и наиболее надежный. Можно пойти другим путем и сделать тоже самое с помощью модуля multiprocessing:
pool = multiprocessing.Pool() pool.map(…)
Однако, автор столкнулся со странностями в поведении, при использовании такого подхода совместно с пакетом анализа данных Pandas. pool.map - иногда отказывался работать с большим количеством процессов, но в других случаях все было нормально. Такой проблемы не было при использовании concurrent.futures и подозрения падают именно на внутреннее взаимодействие передачи данных между процессами.
Используя ProcessPoolExecutor мы создаем новые процессы в обход GIL(Global Interpreter Lock). Так же, GIL в Python не дает возможности использовать потоки(Threads) для параллельной обработки в разных процессах, как это сделано в других языках.
Вы можете работать с потоками используя ThreadPoolExecutor:
with concurrent.futures.ThreadPoolExecutor() as executor: result = executor.map(function, iterable)
Таким простым способом, переименование класса в ThreadPoolExecutor, мы сделали выполнение задач в отдельных потоках, а не процессах. При использовании потоков(Threads), GIL не позволит сделать выполнение кода по истине параллельной. Однако, если вы делаете какую то IO-опепацию(запись в файл, сетевой запрос), то это обычно освобождает GIL и фактически вы увидите прирост в производительности. Для примера, вы можете выполнить одновременно 1000 сетевых запросов используя concurrent.futures, и увидите, что скрипт не будет залипать что бы дождаться ответа.
Добавить комментарий