Блог

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) выполнение задач и корутин.

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



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




Читать дальше >>

Список тэгов

    Apps Script      Arrays Java Script      asynchronous code      asyncio      coroutine      Django      Dropdown List      Drop Shipping      Exceptions      GitHub      Google API      Google Apps Script      Google Docs      Google Drive      Google Sheets      multiprocessing      Parsing      Python      regex      Scraping      ssh      Test Driven Development (TDD)      threading      website monitoring      zip