Блог

Threading, Multiprocessing и AsyncIO в Python (часть 6)

Содержание

  1. Введение в Асинхронность
    1. Что такое “асинхронный код”?
    2. Сравнение трёх способов получения асинхронного кода
  2. Потоки (threading)
    1. Потоки: создание и управление
    2. Потоки: примитивы синхронизации: Lock
    3. Потоки: примитивы синхронизации: Lock(продолжение), Event, Condition, Semaphore, Queue 🔒
    4. Использования потоков для скрейпинга вебсайтов 🔒
  3. Мультипроцессы (multiprocessing) 🔒
    1. Мультипроцессы : создание и управление 🔒
    2. Мультипроцессы: примитивы синхронизации 🔒
  4. Пакет asyncio
    1. Генератор, как асинхронная функция
    2. Корутины (Coroutines), Задачи (Tasks) и Цикл событий (Event Loop)
    3. Переход от генераторов к корутинам и задачам 🔒
    4. Скрейпинг сайтов с помощью пакета aiohttp 🔒
    5. Работа с файлами с помощью пакета aiofiles 🔒
    6. Примитивы синхронизации для asyncio 🔒
  5. Дополнительные пакеты и методы создания асинхронности 🔒
    1. Пакет subprocess 🔒
    2. Пакет concurrent.futures 🔒
    3. Сокеты - метод timeout() и пакет select 🔒
    4. Пакеты curio и trio 🔒

4. Asyncio
4.2 Корутины (Coroutines), Задачи (Tasks) и Цикл событий (Event Loop)

Аналогом генераторов в asyncio будет корутина (coroutine) - функция, выполнение которой можно приостановить и затем вновь запустить. Как видим, под определение корутины полностью подходит и генератор. За одним исключением: корутина обладает свойством awaitable - способностью (возможностью) быть использована с оператором await.

Для запуска корутин нам потребуется Цикл Событий (Event Loop) - ключевой элемент в asyncio, который запускает асинхронные задачи. В пакете asyncio цикл событий представлен достаточно широким инструментарием различный низкоуровневых методов и функций. Однако важно помнить, что для реализации большинства практических задач более чем достаточно использование высокоуровневой функции asyncio.run().
https://docs.python.org/3/library/asyncio-eventloop.html#event-loop

Давайте рассмотрим простейший пример:

import asyncio


async def cor1():
    print('cor1')


asyncio.run(cor1())

Все попытки запустить корутину cor1() как обычную функцию приведут к ошибке: RuntimeWarning: coroutine 'cor1' was never awaited. Что говорит о том, запустить корутину означает сообщить циклу событий о том, что корутина поставлена в ожидание запуска с помощью оператора await. Иными словами, конструкций await cor1() мы, как бы, сообщаем циклу событий примерно следующее: “Мы готовы запустить корутину cor1() в этом месте программы. Пожалуйста, сделай это при первой возможности”.

В нашем предыдущем примере функция syncio.run() уже содержит в себе этот оператор неявно, поэтому код запускается без ошибки. Однако мы можем запустит корутину явно из другой корутины:

import asyncio


async def cor1():
    print('cor1')


async def main():
    await cor1()
    print('The cor1 coroutine has been started.')


asyncio.run(main())

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

Кстати, обратите внимание, что задержка в нашем примере берётся не из пакета time, а из пакет asyncio. Объяснение тут следующее: в принципе, в корутине мы можем применять любые функции. Однако, если мы хотим избежать блокировки внутри корутины, эта функция должно обладать свойством awaitable (“Быть awaitable“ - это корректное определение. К сожалению, гораздо чаще можно услышать “функция должна быть асинхронной”)

import time
import asyncio
from my_deco import async_time_counter


async def cor1(start):
    print(f'cor1 before with delay: {time.time() - start:0.0f}')
    await asyncio.sleep(1)
    print(f'cor1 after with delay: {time.time() - start:0.0f}')


async def cor2(start):
    print(f'cor2 before with delay: {time.time() - start:0.0f}')
    await asyncio.sleep(2)
    print(f'cor2 after with delay: {time.time() - start:0.0f}')


@async_time_counter
async def main():
    start = time.time()
    await cor1(start)
    await cor2(start)
    print(f'and all with delay: {time.time() - start:0.0f}')


asyncio.run(main())

Как видим, фокус не удался - корутины выполнились последовательно, а не параллельно:

======== Script started ========
cor1 before with delay: 0
cor1 after with delay: 1
cor2 before with delay: 1
cor2 after with delay: 3
and all with delay: 3
======== Script execution time: 3.00 ========

Это произошло от того, что для параллельного выполнения корутин мало того, что всё функции внутри корутин были бы awaitable-объектами. Корутины необходимо передать в цикл событий не как корутины, а как задачи. Сделать это можно несколькими способами.

Во-первых, это можно сделать явно, превратив корутину в задачу с помощью функции asyncio.create_task() :

import time
import asyncio
from my_deco import async_time_counter


async def cor1(start):
    print(f'cor1 before with delay: {time.time() - start:0.0f}')
    await asyncio.sleep(1)
    print(f'cor1 after with delay: {time.time() - start:0.0f}')


async def cor2(start):
    print(f'cor2 before with delay: {time.time() - start:0.0f}')
    await asyncio.sleep(2)
    print(f'cor2 after with delay: {time.time() - start:0.0f}')


@async_time_counter
async def main():
    start = time.time()
    task1 = asyncio.create_task(cor1(start))
    task2 = asyncio.create_task(cor2(start))
    await task1
    await task2
    print(f'and all with delay: {time.time() - start:0.0f}')


asyncio.run(main())

Как видите, нас можно поздравить - теперь общее время выполнения скрипта сократилось до времени выполнения самой “долгой” корутины:

======== Script started ========
cor1 before with delay: 0
cor2 before with delay: 0
cor1 after with delay: 1
cor2 after with delay: 2
and all with delay: 2
======== Script execution time: 2.00 ========

Во-вторых, ровно тоже самое можно сделать и неявно, с помощью замечательной функции asyncio.gather(), которая автоматически превратит в задачи все корутины, переданные в неё как аргументы:

import time
import asyncio
from my_deco import async_time_counter


async def cor1(start):
    print(f'cor1 before with delay: {time.time() - start:0.0f}')
    await asyncio.sleep(5)
    print(f'cor1 after with delay: {time.time() - start:0.0f}')


async def cor2(start):
    print(f'cor2 before with delay: {time.time() - start:0.0f}')
    await asyncio.sleep(2)
    print(f'cor2 after with delay: {time.time() - start:0.0f}')


@async_time_counter
async def main():
    start = time.time()
    """
    From tutorial: 
        awaitable asyncio.gather(*aws, return_exceptions=False)
    "If any awaitable in aws is a coroutine, it is automatically scheduled as a Task."
    https://docs.python.org/3/library/asyncio-task.html#asyncio.gather    
    """
    await asyncio.gather(cor1(start), cor2(start))
    print(f'and all with delay: {time.time() - start:0.0f}')


asyncio.run(main())

И, в-третьих, в Python версии 3.11 появилась возможность выполнить превращение корутин в задачи ещё более изящно - с помощью asyncio.TaskGroup():

import time
import asyncio
from my_deco import async_time_counter


async def cor1(start):
    print(f'cor1 before with delay: {time.time() - start:0.0f}')
    await asyncio.sleep(5)
    print(f'cor1 after with delay: {time.time() - start:0.0f}')


async def cor2(start):
    print(f'cor2 before with delay: {time.time() - start:0.0f}')
    await asyncio.sleep(2)
    print(f'cor2 after with delay: {time.time() - start:0.0f}')


@async_time_counter
async def main():
    start = time.time()
    async with asyncio.TaskGroup() as tg:
        task1 = tg.create_task(cor1(start))
        task2 = tg.create_task(cor2(start))
    print(f'and all with delay: {time.time() - start:0.0f}')


asyncio.run(main())

Таким образом, мы выяснили, что третьим важнейшим элементом пакета asyncio является Задача (Task). Именно задачи позволяют выполняются корутинам параллельно, точнее, квази-параллельно (concurrently).
https://docs.python.org/3/library/asyncio-task.html#:~:text=a%20coroutine%20function.-,Tasks,%23%20with%20%22main().

Итак, подводя итог всему сказанному в этой теме, заметим, что минимальный набор для создания асинхронного кода с помощью пакета asyncio включает в себя:
  • Корутины - awaitable функции, выполнение которых можно приостановить и продолжить.
  • Задачи - корутины, которые получают возможность быть запущенными квази-параллельно (concurrently).
  • Цикл событий, который организует и диспетчеризирует квази-параллельное (concurrently) выполнение задач и корутин.

Более подробно со всеми деталями этой темы вы сможете познакомиться из этого видео:



Переход на следующую тему