8.1. Кооперативная многозадачность¶
Модель планирования в asyncio — кооперативная, а не вытесняющая. Это различие является важнейшей мысленной моделью, на которой строится весь остальной раздел, поэтому стоит разобраться с ним прежде, чем появится какой-либо код.
8.1.1. Вытесняющая и кооперативная многозадачность¶
Вытесняющий планировщик — такой, какой настольная операционная система использует для одновременной работы множества программ — может приостановить выполняющийся в данный момент фрагмент кода в любой момент и переключиться на другой. Выполняющемуся коду не нужно делать ничего особенного; планировщик прерывает его сам. Это делает вытесняющее планирование очень гибким (ни один фрагмент кода не может, будучи медленным, лишить ресурсов остальные), но также означает, что любую общую переменную приходится тщательно защищать, поскольку переключение может произойти где угодно — даже посреди записи значения или на полпути чтения списка.
Кооперативный планировщик может переключаться между фрагментами кода только в тех точках, где выполняющийся в данный момент фрагмент явно возвращает управление. В asyncio такими точками являются каждый await и каждый вызов корутины, которая внутренне передаёт управление (чаще всего asyncio.sleep()). Между двумя await выполняющаяся корутина владеет процессором единолично.
Из этого вытекают два следствия:
Корутина, которая никогда не выполняет await, никогда не приостанавливается. Если корутина находится в плотном цикле без
awaitвнутри, она монополизирует планировщик, и ничто другое не продвигается вперёд. Решение — выполнитьawait asyncio.sleep_ms(0)(или другой ожидающий вызов) в подходящей точке цикла.Общее состояние безопасно между await. Две корутины не могут вклиниться друг в друга посреди операции, не содержащей
await. Тот вид повреждения данных, который возникает, когда вытеснение попадает в середину многошагового обновления — один фрагмент кода читает значение, пока другой на полпути его изменения, — здесь просто не может произойти. Координация между корутинами всё ещё нужна, когда нескольким из них приходится совместно использовать ресурс через await, но проблема вклинивания посреди строки тут неприменима.
8.1.2. Три уровня¶
Каждый скрипт asyncio построен из одних и тех же трёх уровней. Следующие две страницы рассматривают их подробно; вот метки, которые стоит держать в уме при чтении.
Корутины — функции, объявленные с
async def, каждая из которых представляет собой самодостаточную единицу работы, выполняющую await там, где это уместно. Обзор Python познакомил с ключевыми словамиasync/await; в asyncio именно так корутина возвращает управление планировщику.Задачи — обёртка, которую
asyncio.create_task()накладывает на корутину, чтобы запланировать её выполнение параллельно с текущей. Приложение обычно создаёт несколько задач для долго выполняющихся заданий (цикл снимков, сетевой клиент, читатель UART, …).Цикл событий — движок в основе, который отслеживает, какие корутины ожидают, а какие готовы к выполнению, переключаясь между задачами на каждом
await. Приложение не пишет цикл само; оно передаёт корутину верхнего уровня вasyncio.run(), и цикл управляет всем оттуда.
Когда приложение описано таким образом — как небольшой набор корутин, скомпонованных циклом событий, — многозадачность становится свойством формы программы, а не тем, чем приложению приходится управлять шаг за шагом.