Асинхронный Python: Различные Формы Параллелизма

С появлением Python 3 было немало шума по поводу «асинхронности» и «параллелизма», можно предположить, что Python недавно представил эти функции/концепции.

Но это неправда.

Мы использовали эти операции много раз.

Кроме того, новички могут подумать, что asyncio — единственный или лучший способ воссоздать и использовать асинхронные/параллельные операции.

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



Значение терминов:

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

Синхронный и асинхронный: В синхронный В операциях задачи выполняются одна за другой.

В асинхронный задачи могут запускаться и завершаться независимо друг от друга.

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

Асинхронные задачи не блокировать (не заставлять вас ждать завершения задачи) операции и обычно работать в фоновом режиме.

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

Вам необходимо отправить письмо своему менеджеру перед вылетом.

В синхронном режиме вы сначала позвоните в турфирму, и если вас попросят подождать, вы будете ждать, пока вам ответят. Затем вы начнете писать письмо своему руководителю.

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

[синхронное исполнение, прим.

переводчик] Но, если ты умный, то тебя пока попросили подождать [повесьте трубку, прим.

переводчик] вы начнете писать электронное письмо, и когда с вами снова заговорят, вы приостановите написание, поговорите, а затем закончите письмо.

Вы также можете попросить друга позвонить в агентство и написать письмо самостоятельно.

Это асинхронно, задачи не блокируют друг друга.

Параллелизм и параллелизм: Параллелизм подразумевает совместное выполнение двух задач.

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

агентство.

Этот конкурентоспособность .

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

параллельный .

Параллелизм, по сути, является формой параллелизма.

Но параллелизм зависит от аппаратного обеспечения.

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

Они просто делят между собой процессорное время.

Тогда это параллелизм, а не параллелизм.

Но когда у нас есть несколько ядер [как и друг в предыдущем примере, который является вторым ядром, прим.

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

Подведем итоги:

  • Синхронность: блокирует операции (блокировка)
  • Асинхронный: не блокирует операции (неблокирующий).

  • Конкурентоспособность: совместный прогресс (совместный)
  • Параллелизм: параллельный прогресс (параллельный)
Параллелизм подразумевает параллелизм.

Но параллелизм не всегда означает параллелизм.



Потоки и процессы

Python поддерживает потоки уже очень давно.

Потоки позволяют выполнять операции одновременно.

Но есть проблема с Глобальная блокировка переводчика (GIL) из-за чего потоки не могли обеспечить настоящий параллелизм.

И все же с появлением многопроцессорность с Python можно использовать несколько ядер.

Потоки Давайте посмотрим на небольшой пример.

В следующем коде функция рабочий будет выполняться в нескольких потоках асинхронно и одновременно.

  
  
  
  
  
  
   

import threading import time import random def worker(number): sleep = random.randrange(1, 10) time.sleep(sleep) print("I am Worker {}, I slept for {} seconds".

format(number, sleep)) for i in range(5): t = threading.Thread(target=worker, args=(i,)) t.start() print("All Threads are queued, let's see when they finish!")

И вот пример вывода:

$ python thread_test.py All Threads are queued, let's see when they finish! I am Worker 1, I slept for 1 seconds I am Worker 3, I slept for 4 seconds I am Worker 4, I slept for 5 seconds I am Worker 2, I slept for 7 seconds I am Worker 0, I slept for 9 seconds

Таким образом мы запустили 5 потоков для совместной работы и после их запуска (то есть после запуска рабочей функции) операция не ждет Потоки завершаются перед переходом к следующему оператору печати.

Это асинхронная операция.

В нашем примере мы передали функцию конструктору Thread. Если бы мы захотели, мы могли бы реализовать подкласс с методом (стиль ООП).

Дальнейшее чтение: Чтобы узнать больше о потоках, воспользуйтесь ссылкой ниже:

Глобальная блокировка переводчика (GIL) GIL был введен, чтобы упростить обработку памяти CPython и обеспечить лучшую интеграцию с C (например, расширения).

GIL — это механизм блокировки, при котором интерпретатор Python одновременно запускает только один поток.

Те.

только один поток может одновременно выполнять байт-код Python. GIL гарантирует, что несколько потоков не выполняются параллельный .

Краткая информация о ГИЛ:

  • Одновременно может выполняться один поток.

  • Интерпретатор Python переключается между потоками для достижения параллелизма.

  • GIL применим к CPython (стандартная реализация).

    Но такие как, например, Jython и IronPython не имеют GIL.

  • GIL делает однопоточные программы быстрыми.

  • GIL обычно не мешает операциям ввода-вывода.

  • GIL упрощает интеграцию непотокобезопасных библиотек C, благодаря GIL у нас есть множество высокопроизводительных расширений/модулей, написанных на C.
  • Для задач, зависящих от процессора, интерпретатор проверяет каждые N тактов и переключает потоки.

    Таким образом, один поток не блокирует другие.

Многие считают GIL слабостью.

Я рассматриваю это как благо, ведь были созданы такие библиотеки, как NumPy и SciPy, занимающие особое, уникальное положение в научном сообществе.

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

Процессы Для достижения параллелизма в Python был добавлен модуль многопроцессорность Который предоставляет API и выглядит очень похоже, если вы использовали резьба ранее.

Давайте просто изменим предыдущий пример.

Теперь модифицированная версия использует Процесс вместо Поток .



import multiprocessing import time import random def worker(number): sleep = random.randrange(1, 10) time.sleep(sleep) print("I am Worker {}, I slept for {} seconds".

format(number, sleep)) for i in range(5): t = multiprocessing.Process(target=worker, args=(i,)) t.start() print("All Processes are queued, let's see when they finish!")

Что изменилось? Я только что импортировал модуль многопроцессорность вместо резьба .

И тогда вместо потока я использовал процесс.

Вот и все! Теперь вместо нескольких потоков мы используем процессы, работающие на разных ядрах ЦП (если, конечно, ваш процессор многоядерный).

С помощью класса Pool мы также можем распределить выполнение одной функции между несколькими процессами для разных входных значений.

Пример из официальных документов:

from multiprocessing import Pool def f(x): return x*x if __name__ == '__main__': p = Pool(5) print(p.map(f, [1, 2, 3]))

Здесь вместо того, чтобы перебирать список значений и вызывать функцию f по одному, мы фактически запускаем функцию в разных процессах.

Один процесс выполняет f(1), другой выполняет f(2), а третий выполняет f(3).

Наконец, результаты снова объединяются в список.

Это позволяет нам разбивать тяжелые вычисления на более мелкие части и запускать их параллельно для более быстрых вычислений.

Дальнейшее чтение:

модуль concurrent.futures Модуль concurrent.futures большой и упрощает написание асинхронного кода.

Мои любимые ThreadPoolExecutor И ProcessPoolExecutor .

Эти исполнители поддерживают пул потоков или процессов.

Мы отправляем наши задачи в пул, и он запускает их в доступном потоке/процессе.

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

А вот пример ThreadPoolExecutor:

from concurrent.futures import ThreadPoolExecutor from time import sleep def return_after_5_secs(message): sleep(5) return message pool = ThreadPoolExecutor(3) future = pool.submit(return_after_5_secs, ("hello")) print(future.done()) sleep(5) print(future.done()) print(future.result())

У меня есть статья о concurrent.futures masnun.com/2016/03/29/python-a-quick-introduction-to-the-concurrent-futures-module.html .

Это может быть полезно при более глубоком изучении этого модуля.

Дальнейшее чтение:



Асинсио — что, как и почему?

У вас, вероятно, возник вопрос, который возникает у многих людей в сообществе Python: что нового дает asyncio? Зачем нужен был другой метод асинхронного ввода-вывода? Разве у нас уже не было потоков и процессов? Давай посмотрим! Зачем нам нужен asyncio? Процессы очень дорогие [с точки зрения потребления ресурсов, прим.

переводчик] для создания.

Поэтому потоки в основном выбираются для операций ввода-вывода.

Мы знаем, что ввод-вывод зависит от внешних факторов: медленные диски или неприятные задержки в сети делают ввод-вывод часто непредсказуемым.

Теперь предположим, что мы используем потоки для операций ввода-вывода.

3 потока выполняют различные задачи ввода-вывода.

Интерпретатору придется переключаться между конкурирующими потоками и по очереди уделять каждому из них некоторое время.

Назовем потоки Т1, Т2 и Т3. Три потока начали операцию ввода-вывода.

Т3 завершает его первым.

T2 и T1 все еще ждут ввода-вывода.

Интерпретатор Python переключается на T1, но все еще ждет. Хорошо, интерпретатор переходит к T2, который все еще ожидает, а затем переходит к T3, который готов и выполняет код. Вы видите в этом проблему? Т3 был готов, но переводчик сначала переключился между Т2 и Т1 — это влечет за собой затраты на переключение, которых мы могли бы избежать, если бы переводчик сначала переключился на Т3, верно? Что такое асинио? Asyncio предоставляет нам цикл событий и другие интересные вещи.

Цикл событий отслеживает события ввода-вывода и переключает задачи, которые готовы и ожидают операций ввода-вывода.

[цикл событий — программная конструкция, ожидающая прихода и отправляющая события или сообщения в программу, прим.

переводчик] .

Идея очень проста.

Есть цикл событий.

И у нас есть функции, выполняющие асинхронные операции ввода-вывода.

Мы передаем наши функции в цикл обработки событий и просим его выполнить их за нас.

Цикл событий возвращает объект Future как обещание, что мы получим что-то в будущем.

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

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

переводчик] .

Asyncio использует генераторы и сопрограммы для остановки и возобновления задач.

Подробности можно прочитать здесь:

Как использовать асинхронно? Прежде чем мы начнем, давайте посмотрим на пример:

import asyncio import datetime import random async def my_sleep_func(): await asyncio.sleep(random.randint(0, 5)) async def display_date(num, loop): end_time = loop.time() + 50.0 while True: print("Loop: {} Time: {}".

format(num, datetime.datetime.now())) if (loop.time() + 1.0) >= end_time: break await my_sleep_func() loop = asyncio.get_event_loop() asyncio.ensure_future(display_date(1, loop)) asyncio.ensure_future(display_date(2, loop)) loop.run_forever()

Обратите внимание, что синтаксис async/await предназначен только для Python 3.5 и выше.

Давайте пройдемся по коду:

  • У нас есть асинхронная функция display_date, которая принимает в качестве параметров число (в качестве идентификатора) и цикл событий.

  • Функция имеет бесконечный цикл, который прерывается через 50 секунд. Но в этот период она неоднократно набирает время и делает паузу.

    Функция await может ожидать завершения выполнения других асинхронных функций (сопрограмм).

  • Мы передаем функцию в цикл обработки событий (используя метод обеспечения_будущего).

  • Запускаем цикл событий.

Всякий раз, когда происходит вызов await, asyncio знает, что для завершения функции, скорее всего, потребуется некоторое время.

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

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



Делаем правильный выбор

Мы только что прошли через самые популярные формы конкуренции.

Но остается вопрос – что выбрать? Это зависит от вариантов использования.

По своему опыту я склонен следовать этому псевдокоду:

if io_bound: if io_very_slow: print("Use Asyncio") else: print("Use Threads") else: print("Multi Processing")

  • Ограничение ЦП => Многопроцессорная обработка
  • Ограничение ввода-вывода, быстрый ввод-вывод, ограниченное количество соединений => многопоточность
  • Ограничение ввода-вывода, медленный ввод-вывод, множество подключений => Asyncio
[Примечание переводчика] Теги: #python #программирование #изучение языков #параллелизм #параллелизм
Вместе с данным постом часто просматривают:

Автор Статьи


Зарегистрирован: 2019-12-10 15:07:06
Баллов опыта: 0
Всего постов на сайте: 0
Всего комментарий на сайте: 0
Dima Manisha

Dima Manisha

Эксперт Wmlog. Профессиональный веб-мастер, SEO-специалист, дизайнер, маркетолог и интернет-предприниматель.