Pull to refresh

Comments 7

Спасибо большое за статью! Подскажите, а если использовать ProcessPoolExecutor и запускать синхронную функцию eval в отдельных процессах создавая дополнительный цикл событий и пул подключений к БД в такой ситуации не даст никаких преимуществ? (сразу извиняюсь за вопрос от дилетанта, просто сейчас как раз разбираюсь с асинхронщиной)

Хороший вопрос.

Смотрите, если попытаться напрямую вызвать ProcessPoolExecutor и выполнять что-либо в нем, то в момент ожидания завершения дочерних процессов, мы все равно будем блокировать основной EventLoop, т.е. не сможем переключить контекст процесса и обрабатывать другие запросы. 

По сути, нам нужно вернуть контроль циклу событий с помощью await. Этого можно добиться засчет loop.run_in_executor (https://docs.python.org/3/library/asyncio-eventloop.html#asyncio.loop.run_in_executor) и в него передать ProcessPoolExecutor(). Тогда мы сможем распараллелить выполнение CPU-bound операции, при этом не блокируя основной цикл событий. Вот здесь приведен хороший пример (https://stackoverflow.com/questions/53304695/python-invoke-a-process-pool-without-blocking-the-event-loop). 

Но тут есть свои подводные камни. Во-первых, мы не сможем создать бесконечно много дочерних процессов, по-дефолту max_workers=кол-во ядер на машине. Т.е. мы все равно упираемся в ограничение числа ядер, и т.к. правил у нас >> ядер, то все равно пришлось бы висеть в ожидании. Во-вторых, при таком подходе возникает нагрузка на CPU, когда каждый из запущенных инстансов приложения будет спаунить по несколько дочерних процессов. В-третьих, простота реализации и результат. Переписать код на использование мультипроцессорности и учесть все нюансы будет сложнее/дольше, чем подкорректировать код на использование кэширования. Самое главное, что мы не получим такой же прирост производительности, т.к. нам все равно пришлось бы вычислять каждое правило (хоть и параллельно), в то время как при кэшировании просто достаем уже посчитанный результат.

Вообще асинхронность предполагает, что вы работаете в одном процессе, в одном потоке и получаете результат засчет переключения контекста в момент ожидания выполнения i/o-bound операций. Мультипрцессорность немного про другое (распараллелить выполенение cpu-bound операций). Мне не приходилось пока что совмещать два этих подхода вместе, возможно, это в принципе плохая практика :)

Резюмирую. При использовании ProcessPoolExecutor внутри loop.run_in_executor мы бы смогли переключать контекст процесса и не блокировать EventLoop => смогли обрабатывать другие внешние запросы. Относительно самой функции, в которой вычисляются правила - выигрыш был бы незначительный (по сравнению с тем, чтобы просто в цикле вызывать eval), т.к. правил сильно больше кол-ва ядер => так же нужно ожидать, когда посчитаются все правила, притом, что будет тратиться время на создание и удаление дочерних процессов. Самое оптимальное - посчитать один раз и закешировать.

Спасибо за ответ! А если сделать список функций, а затем через run_in_executer получить список эвэйтбл объектов, которые в свою очередь можно отправить в asyncio.as_ completed в этом случае контекст будет переключаться на моменте получения результатов, которые ожидаются await? Вроде такой вариант может немного увеличить узкое место при обработке данных полученных из БД? P.S. Я понял, что ваш вариант проще, быстрее и логичнее просто хочется разобраться в теме)

Да, все верно, можно сделать наподобие такого:

loop = asyncio.get_running_loop()
with concurrent.futures.ProcessPoolExecutor() as executor:
    futures = [loop.run_in_executor(executor, self._eval_rule, rule) for rule in rules]
    results = await asyncio.gather(*futures)

В данном случае лучше использовать asyncio.gather, т.к. не обязательно сразу получать/использовать результат eval и можно сразу "параллельно" выполнять вычисления. В таком случае будет переключаться контекст процесса, и мы сможем обрабатывать другие подключение. Тем самым мы частично решим проблему узкого места. Частично - потому что мы все равно будем ожидать вычисления всех правил, но при этом не будем блокировать основной цикл.

В общем, да, таким способом можно получить небольшой прирост в производительности.

Спасибо большое за ответ! Ждём новых курсов от KTS)

Sign up to leave a comment.