Функция run() — создание и управление циклом событий



Функция asyncio.run(main()) служит для создания и управления циклом событий. При ее запуске из переданной корутины (здесь main()) создается и запускается первая задача нового цикла событий с именем Task-1. Ее набор инструкций является основным набором инструкций для цикла событий, т.е. цикл событий функционирует, пока не будут выполнены все инструкции задачи Task-1. После выполнения этой задачи все незавершенные задачи (если они есть) будут отменены и цикл событий будет завершен. Эта функция не может быть вызвана дважды в одном потоке и служит основной точкой входа и выхода из цикла событий. При написании парсеров мы будем запускать её только один раз.

Код ниже является элементарным примером использования асинхронного кода с использованием асинхронной функции asyncio.sleep(n) (эта функция реализует все признаки ожидания выполнения I/O-bound операции и поэтому часто используется в примерах как ее имитация).

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

Пример 1:

import asyncio
import time

async def two():
    name = asyncio.current_task().get_name()
    print(f'* {name}: hello {time.perf_counter() - start_time:.4f} секунды')
    await asyncio.sleep(1.5)
    print(f'* {name}: world {time.perf_counter() - start_time:.4f} секунды')

async def one():
    # Начало выполнения Task-1 (one())
    print(f'--- start! {time.perf_counter() - start_time:.4f} секунды ---')

    # Выводим название текущей задачи
    print(f'--- Текущая задача: {asyncio.current_task().get_name()}---')

    # Создаем задачу с именем Задача-1
    task = asyncio.create_task(two(), name='Задача-1')

    # Здесь происходит приостановка выполнения one() на 1 секунду и запуск task
    await asyncio.sleep(1) # Имитация ожидания выполнения I/O-bound операции

    # Ожидание завершено, продолжаем выполнение кода в Task-1
    print(f'--- end sleep(1) {time.perf_counter() - start_time:.4f} секунды ---')

    # Ожидание завершения задачи task
    await task

    # Выполнение последней инструкции в Task-1. После завершения Task-1 цикл событий будет завершен
    print(f'--- end! {time.perf_counter() - start_time:.4f} секунды ---')

# Фиксируем время начала запуска программы
start_time = time.perf_counter()

# Запуск асинхронной программы
asyncio.run(one())

# Цикл событий завершен
print(f'Время выполнения программы: {time.perf_counter() - start_time:.4f} секунды')

Вывод:

— start! 0.0018 секунды —
— Текущая задача: Task-1—
* Задача-1: hello 0.0019 секунды
— end sleep(1) 1.0109 секунды —
* Задача-1: world 1.5202 секунды
— end! 1.5204 секунды —
Время выполнения программы: 1.5225 секунды

Что происходит при выполнении:

При запуске  run(one()) создаётся цикл событий. Из корутины  one() создается и запускается задача Task-1.

Начинается выполнение ее инструкций:

Фиксируем начало выполнения Task-1 (one())

— start! 0.0016 секунды —

Проверяем, что выполняется именно Task-1

— Текущая задача: Task-1—

Создаем новую задачу task:

task = asyncio.create_task(two(), name=’Задача-1′)

После создания задача планируется циклом событий к запуску, но не запускается! Сам запуск задачи произойдет только при переключении контекста (обычно на ключевом слове await).

task — это объект типа Task (задача), а Задача-1 — это значение аргумента name этого объекта (его имя).

Используем имитацию ожидания I/O-bound операции,

await asyncio.sleep(1)

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

Цикл событий позволяет захватить GIL другой задаче, которая готова начать/продолжить свое выполнение. Собственно вот этот процесс и называется переключением контекста. А так как у нас такая задача есть: task, то начинается ее выполнение.

Получаем имя выполняемой задачи, выводим ее первое сообщение:

* Задача-1: hello 0.0019 секунды

В task вызываем свое ожидание

await asyncio.sleep(1.5)

Теперь уже выполнение task приостанавливается примерно на полторы секунды, GIL отпущен.

Вот только до завершения ожидания в Task-1 захватить его некому, так как нет других задач, которые конкурировали бы за него.

Ожидание в Task-1 завершается.

​Что происходит при завершении I/O-bound операции. Операционная система (именно ею выполняются I/O-bound операции) передает в цикл событий результат выполнения операции, цикл событий находит задачу, которая ждет их, и передает этот результат в соответствующую задачу. Как только это происходит, задача вновь готова захватить GIL и продолжить свою работу.

GIL захвачен, Task-1 возобновляет свое выполнение.

— end sleep(1) 1.0109 секунды —

Выполнение доходит до строчки await task. Так как задача task уже была запущена раньше, то здесь просто осуществляется ожидание завершения выполнения задачи task. Это значит, что выполнение Task-1 будет приостановлено до завершения task, GIL вновь отпущен, но до завершения оставшихся 0.5 секунды ожидания в task , захватывать его некому.

Ожидание в task завершается: происходит все то же самое, что происходит при завершении I/O-bound операции, GIL захвачен task, она возобновляет свою работу.

* Задача-1: world 1.5202 секунды

На этом выполнение task завершено, GIL отпущен.

Так как ожидание выполнения task в Task-1 завершается, то Task-1 захватывает GIL и продолжает свое выполнение.

— end! 1.5204 секунды —

Последняя инструкция в Task-1 выполнена, задача Task-1 завершила свою работу.

Цикл событий завершил свою работу

Время выполнения программы: 1.5225 секунды

Обратите внимание на общее время выполнения программы, оно едва превышает время выполнения самой «долгой» задачи, а это значит, что ожидание выполнения I/O-bound операций в этом примере происходило одновременно!

На таком простом примере можно понять, как происходит переключение между awaitable-объектами. Если вы не поняли, как это работает, советую запустить код у себя и поэкспериментировать с ним. Я думаю, у вас всё получится!

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

Пример 2:

import asyncio
import time

async def two():
    name = asyncio.current_task().get_name()
    print(f'* {name}: hello {time.perf_counter() - start_time:.4f} секунды')
    await asyncio.sleep(1.5)
    print(f'* {name}: world {time.perf_counter() - start_time:.4f} секунды')

async def one():
    print(f'--- start! {time.perf_counter() - start_time:.4f} секунды ---')
    print(f'--- Текущая задача: {asyncio.current_task().get_name()}---')
    task = asyncio.create_task(two(), name='Задача-1')
    await asyncio.sleep(1)
    print(f'--- end sleep(1) {time.perf_counter() - start_time:.4f} секунды ---')
    await task
    print(f'--- end! {time.perf_counter() - start_time:.4f} секунды ---')

# Фиксируем время начала запуска программы
start_time = time.perf_counter()

# Получаем цикл событий
loop = asyncio.get_event_loop()

# Передаем корутину one() для запуска в этот цикл событий
loop.run_until_complete(one())

# Цикл событий завершен
print(f'Время выполнения программы: {time.perf_counter() - start_time:.4f} секунды')

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

DeprecationWarning: There is no current event loop loop = asyncio.get_event_loop()



Добавить комментарий

;-) :| :x :twisted: :smile: :shock: :sad: :roll: :razz: :oops: :o :mrgreen: :lol: :idea: :grin: :evil: :cry: :cool: :arrow: :???: :?: :!: