Добавить в корзинуПозвонить
Найти в Дзене
АЯй Компания

Параллелизм в Python: как ускорить код с помощью потоков

Последовательное выполнение не всегда имеет смысл. Например, нет смысла оставлять программу бездействующей, если выходы не зависят друг от друга. Это основная идея параллелизма - тема, о которой вы многое узнаете сегодня.
В этой статье вы узнаете, как ускорить код Python, выполняя задачи одновременно. Имейте в виду - одновременное выполнение не означает одновременное. Для получения дополнительной
Оглавление

Последовательное выполнение не всегда имеет смысл. Например, нет смысла оставлять программу бездействующей, если выходы не зависят друг от друга. Это основная идея параллелизма - тема, о которой вы многое узнаете сегодня.

В этой статье вы узнаете, как ускорить код Python, выполняя задачи одновременно. Имейте в виду - одновременное выполнение не означает одновременное. Для получения дополнительной информации об одновременном (параллельном) выполнении ознакомьтесь с этой статьей.

Эта статья построена следующим образом:

Введение в многопоточность

Реализация потоковой передачи - отправка 1000 запросов

Результаты

Заключение

Вы можете скачать исходный код этой статьи здесь.

Введение в многопоточность

Итак, что же такое потоки? Проще говоря, это концепция программирования, которая позволяет запускать код одновременно. Параллелизм означает, что приложение выполняет более одной задачи - первая задача не должна завершаться до запуска второй.

Допустим, вы делаете несколько запросов к какому-то веб-API. Нет смысла отправлять один запрос, ждать ответа и повторять один и тот же процесс снова и снова.

Параллелизм позволяет отправлять второй запрос, пока первый ожидает ответа. Следующее изображение должно лучше, чем слова, объяснить идею последовательного и параллельного выполнения:

-2

Обратите внимание, что одна точка представляет собой небольшую часть задачи. Параллелизм может помочь ускорить время выполнения, если задача какое-то время простаивает (подумайте о типе связи запрос-ответ).

Теперь вы знаете основы работы с потоками в теории. В следующем разделе показано, как реализовать это на Python.

Реализация потоковой передачи - отправка 1000 запросов

Распределение потоков чрезвычайно просто реализовать с помощью Python. Но сначала опишем задачу.

Мы хотим объявить функцию, которая делает запрос GET к конечной точке и извлекает некоторые данные JSON. Веб-сайт JSONPlaceholder идеально подходит для этой задачи, поскольку он служит фиктивным API. Мы повторим процесс 1000 раз и проверим, как долго наша программа практически ничего не делает - ждет ответа.

Давайте сначала проведем тест без многопоточности. Вот сценарий:

import time
import requests

URL = 'https://jsonplaceholder.typicode.com/posts'

def fetch_single(url: str) -> None:
print('Fetching...')
requests.get(url)
print('Fetched!')


time_start = time.time()
for _ in range(1000):
fetch_single(URL)

time_end = time.time()
print(f'\nAll done! Took {round(time_end - time_start, 2)} seconds')

Я считаю, что в приведенном выше сценарии нет ничего незнакомого. Мы повторяем запрос 1000 раз и отслеживаем время начала и окончания. Операторы печати в функции fetch_single () находятся здесь по единственной причине - чтобы увидеть, как программа ведет себя при выполнении.

Вот результат, который вы увидите после запуска этого скрипта:

-3

Как видите, одна задача должна завершиться, чтобы могла начаться другая. Не оптимальное поведение для нашего типа проблемы.

Давайте теперь реализуем потоки. Скрипт будет выглядеть более или менее идентично, с некоторыми отличиями:

Нам нужен дополнительный импорт - concurrent.futures

Мы не печатаем последний оператор, а возвращаем его

ThreadPoolExecutor () используется для одновременной отправки и выполнения задач.

Вот весь фрагмент:

import time
import requests
import concurrent.futures

URL = 'https://jsonplaceholder.typicode.com/posts'

def fetch_single(url: str):
print('Fetching...')
requests.get(url)
return 'Fetched!'

time_start = time.time()

with concurrent.futures.ThreadPoolExecutor() as tpe:
results = [tpe.submit(fetch_single, URL) for _ in range(1000)]
for f in concurrent.futures.as_completed(results):
print(f.result())

time_end = time.time()
print(f'\nAll done! Took {round(time_end - time_start, 2)} seconds')

После выполнения вы увидите результат, похожий на этот:

-4

Это все здорово, но есть ли реальное улучшение скорости? Давайте рассмотрим это дальше.

Результаты

К настоящему времени вы знаете разницу между последовательным и параллельным выполнением и знаете, как преобразовать свой код для одновременного выполнения вызовов функций.

Теперь сравним производительность во время выполнения. На следующем изображении показано время выполнения в секундах для вышеупомянутой задачи - выполнение 1000 вызовов API:

-5

Как видите, время выполнения сокращается примерно в 13 раз - по крайней мере, неплохо.

Заключение

Сегодня вы многому научились - от базовой теории, лежащей в основе многопоточности и параллельного выполнения, до того, как вы можете «преобразовать» непараллельный код в параллельный.

Имейте в виду, что параллелизм - это не полный ответ на вопрос увеличения скорости с помощью Python. Перед реализацией потоковой передачи в вашем приложении, пожалуйста, подумайте, как оно было разработано. Подаются ли выходные данные одной функции напрямую в качестве входных данных для другой? В таком случае параллелизм, вероятно, не то, что вам нужно.

С другой стороны, если ваше приложение большую часть времени простаивает, «одновременное выполнение» может быть именно тем термином, которого вы ждали.

Спасибо за прочтение.