Как писать параллельный код с помощью Python Future
тл;др;
Использование параллелизма для ускорения работы в Python довольно просто с помощью
concurrent.futures
модуль. Тем не менее, это не серебряная пуля, и нужно знать когда использовать его.
Все началось с генераторов…
В предыдущем посте я упомянул, как вы можете использовать генераторы Python, чтобы избежать дополнительных вызовов службы, сэкономив время и ресурсы. Вот краткое изложение:
def has_facebook_account(user_email):
...
def has_github_account(user_email):
...
def has_twitter_account(user_email):
...
def has_social_account(user_email):
calls = [
has_facebook_account,
has_github_account,
has_twitter_account,
]
return any((call(user_email) for call in calls))
Довольно просто, да? any
повторяет и оценивает каждый call(user_email)
yield из генератора, пока один из них не вернется True
. Это известно как раннее возвращение — вы фактически экономите время и ресурсы, не совершая лишних звонков.
Некоторые люди дали хорошие отзывы, упомянув, что я должен это сделать. одновременный т.е. я мог бы сделать все звонки одновременно и раннее возвращение как только любой вызов вернул Истинно. «Хорошая идея», — подумал я. Я рад, что есть люди умнее меня.
Если это не ясно Почему Я хотел бы это сделать: предположим has_facebook_account
выполняется слишком долго (как это обычно бывает с любым вводом-выводом и сетевыми операциями из-за высокой задержки) и has_github_account
довольно быстро (например, может быть кэшировано). я бы всегда нужно ждать has_facebook_account
возвращаться до вызов has_github_account
так как элементы генератора будут оцениваться аккуратный. Это не звучит весело.
Сделайте это одновременно!
Я использую питона concurrent.futures
модуль (доступно с версии 3.2). Этот модуль состоит в основном из двух объектов: Executor
и Future
объект. Вы должны прочитать эту документацию, она очень короткая и прямолинейная.
Executor
абстрактный класс Ответственный за планирование задание (или вызываемый) для исполнения асинхронно (или же одновременно). Планирование задачи возвращает Future
объект, который является ссылкой на задачу и представляет ее состояние — в ожидании, завершено или отменено.
Если вы когда-либо работали с Обещания JavaScript до, Future
очень похож на Promise
: вы знаете, что его выполнение будет в итоге быть сделано, но вы не можете знать, когда. Что приятно: это неблокирующий код что означает, что интерпретатору Python не нужно ждать завершения выполнения запланированной задачи перед запуском следующей строки кода.
Таким образом, в нашем сценарии мы могли бы расписание три задачи, по одной для запроса каждой платформы (Facebook, GitHub и Twitter) на предмет адреса электронной почты пользователя. Таким образом, как только любая из этих задач в итоге возвращает значение, я могу раннее возвращение если значение True
так как все, что мы хотим знать, это наличие у пользователя учетной записи на какой-либо из этих платформ.
Разговор дешевый. Покажи мне код.
Приведенный ниже пример кода прост для понимания, но я настоятельно рекомендую вам прочитать строки с комментариями.
from concurrent.futures import ThreadPoolExecutor, as_completed
def has_facebook_account(user_email):
time.sleep(5)
print("Finished facebook after 5 seconds!")
return True
def has_github_account(user_email):
time.sleep(1)
print("Finished github after 1 second!")
return True
def has_twitter_account(user_email):
time.sleep(3)
print("Finished twitter after 3 seconds!")
return False
def has_social_account(user_email):
executor = ThreadPoolExecutor(max_workers=3)
facebook_future = executor.submit(has_facebook_account, user_email)
twitter_future = executor.submit(has_twitter_account, user_email)
github_future = executor.submit(has_github_account, user_email)
future_list = [facebook_future, github_future, twitter_future]
for future in as_completed(future_list):
if future.result() is True:
return True
user_email = "user@email.com"
if __name__ == '__main__':
if (has_social_account(user_email)):
print("User has social account.")
Finished github after 1 second!
User has social account.
Finished twitter after 3 seconds!
Finished facebook after 5 seconds!
Обратите внимание, что хотя facebook_future
занимает больше времени, чем две другие запланированные задачи, но не блокировать выполнение — он продолжает работать в своем собственном потоке. И хотя github_future
является последней запланированной задачей, она завершается первой.
Краткое резюме
Future
это объект, представляющий запланированную задачу, которая будет в итоге финиш.Executor
планировщик задач (как только задача запланирована, она возвращаетFuture
объект).- Это может быть
ThreadPoolExecutor
илиProcessPoolExecutor
(используя потоки против процессов).
- Это может быть
Можно использовать
executor.submit(callable)
чтобы запланировать асинхронное выполнение задачи.as_completed
получает итерациюFuture
объекты и возвращает генератор, в котором каждый полученный элемент является законченный задача.
Когда бы я нет тогда хотите использовать параллелизм?
Как инженеры-программисты, наша работа заключается не только в знании как пользоваться инструментом, но и когда использовать его. Сетевые операции (и вообще операции, связанные с вводом-выводом) обычно являются хорошим местом для использования параллельного кода из-за их задержки. Но всегда есть компромисс…
В примере выше мы поменял производительность на использование ресурсов. Как так? Когда используешь генераторы только худший вариант развития событий в конечном итоге потребляет 3 услуги — по одному вызову для каждой has_<plataform>_account
. Это потому, что мы могли бы вернуться раньше True
если какая-либо служба вернулась True
.
В нашем новом примере с использованием параллелизма мы всегда потребляя услугу 3 — так как звонки совершаются асинхронно.
«Ах, но это все еще может сэкономить нам много времени!» — скажете вы. Это зависит от Сервисы ты потребляешь. В приведенном выше примере я искусственно создал has_facebook_account
очень медленно — В 5 раз медленнее, чем самая быстрая альтернатива. Но если бы все сервисы имели одинаковое время отклика и если экономия ресурсов был важен (предположим, что вызов каждой службы вызовет действительно тяжелый запрос в базе данных, например), используя синхронный код может быть лучшим подходом.
Ради данных: Facebook закончился 2,7 миллиарда активных пользователей в месяцпока Твиттер имеет около 330 миллионова также GitHub просто 40 миллионов пользователей. Таким образом, весьма вероятно, что вызов has_facebook_account
first будет достаточно в подавляющем большинстве сценариев, так как он вернет True
с гораздо более высокой частотой, чем другие услуги, таким образом, экономя много ненужных вызовов.
Вывод
Знайте, как писать параллельный код, что довольно просто с Python Futures. Но еще важнее: знать когда сделать это. Бывают случаи, когда увеличение производительности не окупает использование ресурсов.
настоятельно советую прочитать документы на concurrent.futures
и Глава 17 о превосходной книге Лучано Рамальо «Свободный Python»..