12.13 Асинхронные возможности Python
Краткий пересказ теории
- Асинхронная парадигма (вариант реализации): образующий цикл, события и обратные вызовы
- Python: вместо обратных вызовов — повторный вход в генератор (см. параметрические генераторы), образующий цикл программируйте сами!
- Асинхронность
последовательное выполнение ограниченных yield фрагментов кода различных генераторов в порядке, который определяет программист.
Образующий цикл согласно какой-то логике выбирает сопрограмма и даёт команду отчёт = сопрограмма.send(команда) для выполнения очередного фрагмента
Сопрограмма получает команду с помощью команда = yield(предыдущий отчёт), анализирует её и выполняет следующий фрагмент кода
В действительности:
Образующий цикл (со своей логикой) уже реализован кем-то
Инструменты управления этой логикой реализованы тем же автором
Поэтому пользователи этой действительности не пишут .send() или явный yield
Основная конструкция — результат = yield from генератор, который возвращает то, что у генератора написано в return
Синтаксический сахар и асинхронный протокол:
Вместо генератора пишем сопрограму с помощью async def. Это тот же генератор, но в нём нельзя пользоваться yield
Вместо yield from пишем результат = await сопрограмма (различие в более удобном синтаксисе и в проверке того, сопрограмма с помощью async def)
- Всё остальное берёт на себя разработчик инструментария:
- Образующий цикл
Инструменты управления образующим циклом, которым разрешено делать прямой yield в него
- …
Asyncio
- Напишем программу сортировки слиянием массива из 16 элементов
Все 15 слияний делать руками с помощью вызова функции слить(начало1, конец1, начало2, конец2)
- Для слияния используется общий глобальный второй массив
Перетащим их в структуру asyncio.run()/await
Для наглядности снабдим каждый шаг сортировки await asyncio.sleep(0.1) и выводом номера «задания» (передадим его в качестве пятого параметра)
Разницы никакой, всё равно запуск последовательный
Для придания асинхронности надо писать свой mainloop. Но он уже есть — это asyncio.run(). Не надо писать свой mainloop, надо пользоваться asyncio.create_task() и asyncio.gather(все 15 слияний)
- Получается какая-то каша вместо сортировки, почему?
Эшелонируем сортировку с помощью нескольких фрагментов вида create_task()… + gather(все_таски_из фрагмента) (палево — их пять)
Эшелонируем сортировку с помощью задания тайм-аута (в пятом параметре будем передавать номер эшелона n и ждать n/c секунд) и общего create_task() / gather()
- Чем этот подход плох?
Задача_1: переписать сортировку с использованием asyncio.Event для синхронизации
- Каждая функция слияния работает так
- Дожидается события конца слияния правой половины
- Дожидается события конца слияния левой половины
- Выполняет слияние
- Выставляет готовность события конца слияния своего интервала
Внутри цикла слияния обязательно должен присутствовать asyncio.sleep(0) — иначе синхронизация не понадобится, и проверить, что она произошла, не получится
- Замечания:
- Часть слияний (те, что по одному элементу) не требуют предварительных других слияний, поэтому они либо это проверяют и не ждут, либо события, которые они ждут, уже помечены случившимися
События можно делать просто по именам (7 штук, типа событие_4_8) и передавать их в качестве параметров сопрограмме слияния
- События можно положить в словарь и научить сопрограмму вычислять, какие именно элементы этого словаря — два входных события, а какой — событие готовности результата
Лично я поленился, и написал events = defaultdict(asyncio.Event), т. е. совместил два предыдущих варианта)
Решение при этом должно представлять собой один большой gather() на все созданные задачи
- Формат ввода/вывода для Д/З:
Ввод: Строка в квадратных скобках из 16 чисел через запятую, пригодная для eval(input()) для типа list
[22, 58, 95, 33, 47, 11, 76, 38, 70, 84, 40, 35, 56, 28, 13, 23]
Вывод: результат работы print(список) (со скобками и запятыми)
[11, 13, 22, 23, 28, 33, 35, 38, 40, 47, 56, 58, 70, 76, 84, 95]
- Каждая функция слияния работает так
Д/З
- Задача_1: доделать, сделать тесты, не забыть закомментировать отладочный код)
Задача_2: (на дом). Переписать сортировку под произвольный (не слишком большой) объём данных
Условие: как в Задаче_1, за исключением того, что на вход подаётся список произвольной длины (обязательна синхронизация посредством Event и использование общего gather())
- Тесты:
Сортированный в обратном порядке список длины 16
- Случайный список длины 65
- Случайный список длины 63
- Случайный список длины 48
- Случайный список длины 1000