Возможно, вы слышали, что Python asyncio - это параллельное, но не параллельное программирование. Но как? Я объясню это на простых примерах.

Давайте начнем с идеального параллельного примера №1.

Асинхронная функция say_after - это пример из официальной документации Python. Он печатает что-то «что» после того, как засыпает «задержкой» секунд.

В основной функции мы создаем две задачи say_after: одна говорит «привет» через 1 секунду, а другая говорит «мир» через 2 секунды. Запустите его, и мы увидим, что это занимает всего 2 секунды, потому что обе задачи выполняются одновременно. Идеально!

Но параллельны ли они? Давайте поиграемся с другими примерами, чтобы понять разницу между параллелизмом и параллелизмом.

Пример №2:

Заменим основную функцию следующим образом.

Как видите, мы добавляем печать после создания задач, чтобы проверить, запускаются ли задачи сразу после создания, и добавляем асинхронный сон через 0,5 секунды после этого в основной функции. Обратите внимание, что main - это задача (точнее, сопрограмма).

Ниже представлен результат:

Во-первых, всего по-прежнему требуется 2 секунды, без изменений. Он выполняет основную задачу и две другие задачи say_after одновременно. Асинхронный сон в основном не блокирует.

Во-вторых, До задержки - после создания задач печатается перед запуском задач say_after. Да! Созданные задачи не запускаются сразу после создания, вместо этого они по расписанию запускаются в так называемом цикле событий. Они запускаются только тогда, когда ожидает основная задача, то есть await asyncio.sleep(0.5) в данном случае. И, насколько я понимаю, вы не можете контролировать последовательность выполнения, то есть приоритеты задач.

Пример №3:

В этом примере мы заменяем asyncio.sleep на time.sleep, который ожидает с блокировкой в ​​main и смотрим, когда запускаются задачи say_after.

Посмотрите, что теперь общее время составляет 2,5 секунды. И задача 1 запускается через 0,5 секунды после создания. Понятно, что задачи не параллельны, т.е. выполняются одновременно.

Пример №4:

Вы можете возразить, что asyncio.sleep следует использовать вместо time.sleep при программировании asyncio. Как насчет того, что основная задача что-то делает и вызывает задержку?

В этом примере мы заменяем time.sleep на цикл, чтобы добавить задержку около 1 секунды в основной задаче.

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

Пример № 5:

Если задача запускается, гарантирует ли это, что она закончится в ожидаемое время? Нет!
Давайте посмотрим на этот пример ниже.

У нас есть asyncio.sleep (0.1) в строке № 7, чтобы разрешить запуск task1 и task2, но добавляем time.sleep (3) в строке № 8, чтобы потом блокировать на 3 секунды.
Вот результат:

Вы видите, что обе задачи начинаются немедленно в строках №3 и №4, но не «говорят» после ожидаемых 1 или 2 секунд, а вместо «говорят» (завершают) через 3 секунды.

Причина в том, что когда say_after ожидает в течение 1/2 секунды, цикл событий возвращается к основной задаче и блокируется там на 3 секунды, прежде чем он сможет вернуться к задачам say_after для продолжения.

Вы можете найти полный демонстрационный файл здесь.

Вывод

Asynicio изо всех сил старается быть параллельным, но не параллельным.

Вы не можете контролировать ни начало, ни конец задачи.

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

task1 = asyncio.create_task(say_after(1, ‘hello’))
await task1

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

Альтернатива

Так как же решить проблему параллелизма? Threading.

Это эквивалентный код, например, №5, в котором функция main блокирует сон на 3 секунды.

А вывод выглядит следующим образом.

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

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

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

Linkedin