Параллелизм в Python

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

Вступление

С течением времени вычислительная техника развивалась, и появлялось все больше и больше способов сделать компьютеры еще быстрее. Что, если вместо выполнения одной инструкции за раз мы также можем выполнять несколько инструкций одновременно? Это означало бы значительное увеличение производительности системы.

Благодаря параллелизму мы можем добиться этого, и наши программы Python смогут обрабатывать еще больше запросов за один раз, что со временем приведет к впечатляющему увеличению производительности.

В этой статье мы обсудим параллелизм в контексте программирования Python, различные формы, которые он принимает, и мы ускорим простую программу, чтобы увидеть прирост производительности на практике.

Что такое параллелизм?

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

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

Параллелизм против параллелизма

Мы определили параллелизм как выполнение задач одновременно, но как он соотносится с параллелизмом и что это такое?

Параллелизм достигается, когда несколько вычислений или операций выполняются одновременно или параллельно с целью ускорения процесса вычислений.

И параллелизм, и параллелизм связаны с одновременным выполнением нескольких задач, но их отличает то, что, хотя параллелизм осуществляется только в одном процессоре, параллелизм достигается за счет использования нескольких процессоров для параллельного выполнения задач.

Поток против процесса против задачи

Вообще говоря, потоки, процессы и задачи могут относиться к частям или единицам работы. Однако в деталях они не так похожи.

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

Например, в приложении для обработки документов один поток может отвечать за форматирование текста, а другой - за автосохранение, а другой - за проверку орфографии.

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

Некоторые из общих различий между процессами и потоками:

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

Задача - это просто набор программных инструкций, загруженных в память.

Многопоточность против многопроцессорности против Asyncio

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

Многопоточность - это способность ЦП выполнять несколько потоков одновременно. Идея состоит в том, чтобы разделить процесс на несколько потоков, которые могут выполняться параллельно или одновременно. Такое разделение обязанностей увеличивает скорость выполнения всего процесса. Например, в текстовом процессоре, таком как MS Word, при использовании происходит много вещей.

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

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

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

AsyncIO или асинхронный ввод-вывод - это новая парадигма, представленная в Python 3 с целью написания параллельного кода с использованием синтаксиса async / await. Он лучше всего подходит для сетевых целей с привязкой к вводу-выводу и высокого уровня.

Когда использовать параллелизм

Преимущества параллелизма лучше всего используются при решении проблем, связанных с процессором или вводом-выводом.

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

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

Лучше всего писать параллельный код, когда ресурсы ЦП или ввода-вывода ограничены, и вы хотите ускорить свою программу.

Как использовать параллелизм

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

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

Как только наше приложение будет настроено на Imgur, мы получим идентификатор клиента и секрет клиента, которые мы будем использовать для доступа к API. Мы сохраним учетные данные в .env поскольку Pipenv автоматически загружает переменные из файла .env

Синхронный сценарий

С этими деталями мы можем создать наш первый скрипт, который просто загрузит кучу изображений в папку downloads

 import os 
 from urllib import request 
 from imgurpython import ImgurClient 
 import timeit 
 
 client_secret = os.getenv("CLIENT_SECRET") 
 client_id = os.getenv("CLIENT_ID") 
 
 client = ImgurClient(client_id, client_secret) 
 
 def download_image(link): 
 filename = link.split('/')[3].split('.')[0] 
 fileformat = link.split('/')[3].split('.')[1] 
 request.urlretrieve(link, "downloads/{}.{}".format(filename, fileformat)) 
 print("{}.{} downloaded into downloads/ folder".format(filename, fileformat)) 
 
 def main(): 
 images = client.get_album_images('PdA9Amq') 
 for image in images: 
 download_image(image.link) 
 
 if __name__ == "__main__": 
 print("Time taken to download images synchronously: {}".format(timeit.Timer(main).timeit(number=1))) 

В этом скрипте мы передаем идентификатор альбома Imgur, а затем загружаем все изображения в этом альбоме с помощью функции get_album_images() . Это дает нам список изображений, а затем мы используем нашу функцию для загрузки изображений и сохранения их в папке локально.

Этот простой пример выполняет свою работу. Мы можем загружать изображения из Imgur, но он не работает одновременно. Он загружает только одно изображение за раз, прежде чем перейти к следующему изображению. На моей машине скрипту потребовалось 48 секунд для загрузки изображений.

Оптимизация с помощью многопоточности

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

 # previous imports from synchronous version are maintained 
 import threading 
 from concurrent.futures import ThreadPoolExecutor 
 
 # Imgur client setup remains the same as in the synchronous version 
 
 # download_image() function remains the same as in the synchronous 
 
 def download_album(album_id): 
 images = client.get_album_images(album_id) 
 with ThreadPoolExecutor(max_workers=5) as executor: 
 executor.map(download_image, images) 
 
 def main(): 
 download_album('PdA9Amq') 
 
 if __name__ == "__main__": 
 print("Time taken to download images using multithreading: {}".format(timeit.Timer(main).timeit(number=1))) 

В приведенном выше примере мы создаем Threadpool и настраиваем 5 разных потоков для загрузки изображений из нашей галереи. Помните, что потоки выполняются на одном процессоре.

Эта версия нашего кода занимает 19 секунд. Это почти в три раза быстрее, чем синхронная версия скрипта.

Оптимизация с помощью многопроцессорности

Теперь мы реализуем многопроцессорность на нескольких процессорах для одного и того же скрипта, чтобы увидеть, как он работает:

 # previous imports from synchronous version remain 
 import multiprocessing 
 
 # Imgur client setup remains the same as in the synchronous version 
 
 # download_image() function remains the same as in the synchronous 
 
 def main(): 
 images = client.get_album_images('PdA9Amq') 
 
 pool = multiprocessing.Pool(multiprocessing.cpu_count()) 
 result = pool.map(download_image, [image.link for image in images]) 
 
 if __name__ == "__main__": 
 print("Time taken to download images using multiprocessing: {}".format(timeit.Timer(main).timeit(number=1))) 

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

Это немного быстрее, чем наша версия, в которой используются потоки, и значительно быстрее, чем наша непараллельная версия.

Оптимизация с помощью AsyncIO

Давайте реализуем тот же сценарий с помощью AsyncIO, чтобы увидеть, как он работает:

 # previous imports from synchronous version remain 
 import asyncio 
 import aiohttp 
 
 # Imgur client setup remains the same as in the synchronous version 
 
 async def download_image(link, session): 
 """ 
 Function to download an image from a link provided. 
 """ 
 filename = link.split('/')[3].split('.')[0] 
 fileformat = link.split('/')[3].split('.')[1] 
 
 async with session.get(link) as response: 
 with open("downloads/{}.{}".format(filename, fileformat), 'wb') as fd: 
 async for data in response.content.iter_chunked(1024): 
 fd.write(data) 
 
 print("{}.{} downloaded into downloads/ folder".format(filename, fileformat)) 
 
 async def main(): 
 images = client.get_album_images('PdA9Amq') 
 
 async with aiohttp.ClientSession() as session: 
 tasks = [download_image(image.link, session) for image in images] 
 
 return await asyncio.gather(*tasks) 
 
 if __name__ == "__main__": 
 start_time = timeit.default_timer() 
 
 loop = asyncio.get_event_loop() 
 results = loop.run_until_complete(main()) 
 
 time_taken = timeit.default_timer() - start_time 
 
 print("Time taken to download images using AsyncIO: {}".format(time_taken)) 

В нашем новом скрипте есть несколько изменений. Во-первых, мы больше не используем requests для загрузки наших изображений, а вместо этого используем aiohttp . Причина этого в том, что requests несовместимы с AsyncIO, поскольку он использует модуль http и sockets

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

Ключевое слово async указывает, что наша функция является сопрограммой (Co-operative Routine) , которая представляет собой фрагмент кода, который можно приостанавливать и возобновлять. Сопрограммы работают одновременно в многозадачном режиме, то есть они выбирают, когда делать паузу и позволять другим выполнять.

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

После нескольких запусков этого сценария версии AsyncIO требуется в среднем 14 секунд для загрузки изображений в альбом. Это значительно быстрее, чем многопоточная и синхронная версии кода, и очень похоже на многопроцессорную версию.

Сравнение производительности

Синхронный Многопоточность Многопроцессорность Asyncio


48 с 19 с 14 с 14 с

Заключение

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

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

Исходный код этого сообщения доступен на GitHub для справки.

comments powered by Disqus