Привет, Хабр! Публикуем пятую часть (первая, вторая, третья, четвёртая) перевода руководства по модулю asyncio в Python. Здесь представлены разделы оригинала с №10 по №13. Сила asyncio в том, что этот модуль позволяет запускать множество корутин в конкурентном режиме. Эти корутины могут быть созданы в виде группы и сохранены, а потом их можно, все вместе, в одно и то же время, запустить. Реализовать такой сценарий работы можно, воспользовавшись функцией asyncio.gather(). Познакомимся с этой функцией. Функция asyncio.gather() позволяет вызывающей стороне группировать объекты, допускающие ожидание.Эти объекты, после группировки, можно запустить в конкурентном режиме. Кроме того — можно организовать ожидание их выполнения. Их выполнение можно и отменить. awaitable asyncio.gather(*aws, return_exceptions=False) Запускает в конкурентном режиме объекты, допускающие ожидание, находящиеся в группе объектов aws. Coroutines and Tasks Эта функция пригодится и для группировки, и для выполнения множества корутин или задач: Функцией asyncio.gather() можно воспользоваться в ситуации, когда есть возможность заранее создать множество задач или корутин, которые потом нужно одновременно запустить и, прежде чем продолжать работу, нужно дождаться завершения работы каждой из них. Это — вполне реальная ситуация в том случае, если требуется получить результат от множества похожих задач, например — от одной и той же задачи или корутины, разные экземпляры которой обрабатывают разные данные. Объекты, допускающие ожидание, можно выполнить в конкурентном режиме. После того, как они возвратят результаты, которых ожидает основная программа —  она сможет продолжить работу. Применение функции gather() даёт больше возможностей, чем обычное ожидание завершения работы задач. Она позволяет рассматривать группу объектов, допускающих ожидание, как один такой объект. Вот какие возможности это открывает: Запуск группы объектов, допускающих ожидание, и ожидание завершения их работы с использованием выражения await. Получение результатов от всех сгруппированных объектов, допускающих ожидание, путём вызова в нужное время метода result(). Выполнение группы объектов, допускающих ожидание, может быть отменено с помощью метода cancel(). Проверка того, завершена ли работа всех сгруппированных объектов, допускающих ожидание, может быть выполнена с помощью метода done(). Выполнить функции-коллбэки можно только тогда, когда все задачи в группе завершат работу. И это — далеко не все возможности функции gather(). Здесь мы подробнее поговорим о способах использования функции asyncio.gather(). Эта функция, в качестве аргументов, принимает один объект, допускающий ожидание, или несколько таких объектов. Вспомните о том, что в роли объекта, допускающего ожидание, может выступать корутина, объект Future, или объект Task. В результате — вызывать функцию gather() можно, передавая ей следующие сущности: Множество задач. Множество корутин. Смешанный набор объектов, некоторые из которых являются задачами, а некоторые — корутинами. Например: Если функции gather() передали объекты Task — они уже будут выполняться, так как планирование выполнения таких объектов — это часть процесса их создания. Эта функция принимает объекты, допускающие ожидание, в виде позиционных аргументов. Нельзя создать список или коллекцию таких объектов и передать их этой функции, так как это приведёт к ошибке: Список объектов, допускающих ожидание, можно передать этой функции в том случае, если он сначала распакован и представлен в виде набора его элементов с использованием оператора «звёздочка» — *: Если этой функции передают корутины — они будут автоматически обёрнуты в объекты Task. Функция gather() не является блокирующей. Она, вместо блокировки выполнения кода, возвращает объект asyncio.Future, который представляет группу объектов, допускающих ожидание. Например: После того, как объект Future будет создан, его выполнение в цикле событий планируется автоматически. Объект, допускающий ожидание, представляющий группу объектов, и все объекты в группе, будут выполнены, как только представится такая возможность. Это значит, что если вызывающая сторона больше ничего не делает — группа объектов, допускающих ожидание, выполнение которых запланировано, будет запущена (исходя из предположения о том, что вызывающая сторона приостанавливает своё выполнение). Это, кроме того, значит и то, что нам не нужно ожидать выполнения объекта Future, возвращённого функцией gather(): Можно подождать выполнения возвращённого объекта Future, что приведёт к ожиданию завершения выполнения всех объектов в группе: После ожидания объекта Future, возвращённого функцией gather(), в нашем распоряжении окажется список значений, возвращаемых объектами, допускающими ожидание. Если эти объекты не возвращают значения, в списке окажется возвращаемое значение, используемое по умолчанию — None. Например: Чаще вышеописанные операции оформляют в виде одной строки кода: При работе с группами корутин распространён такой подход: заранее создают несколько корутин, а позже передают их функции gather(). Это позволяет программе подготовить задачи, которые нужно выполнить в конкурентном режиме, а затем инициировать их конкурентное выполнение и подождать их завершения. Несколько корутин можно собрать в единый список или вручную, или используя механизм спискового включения: После этого можно воспользоваться функцией gather(), обработав с её помощью все корутины из списка. Список, как уже говорилось, нельзя напрямую передать функции gather(), так как это приведёт к возникновению ошибки. Вместо этого данная функция нуждается в том, чтобы каждый объект, допускающий ожидание, был бы передан ей в качестве отдельного позиционного аргумента. Добиться этого можно, распаковав список, передаваемый функции. Сделать это можно с помощью оператора «звёздочка» — *, который выполнит все необходимые действия: Если собрать всё это вместе, то у нас получится пример, демонстрирующий запуск списка заранее подготовленных корутин с использованием функции gather(): При выполнении этого примера запускается корутина main(), которая является точкой входа в программу. Затем эта корутина создаёт список из 10 объектов корутин, используя механизм спискового включения. Этот список передаётся функции gather(), его, с помощью оператора «звёздочка», распаковывают, представляя в виде 10 отдельных значений. Корутина main() ожидает выполнения объекта Future, возвращённого после вызова функции gather(). Выполнение программы приостанавливается, она ждёт завершения выполнения всех запланированных корутин. Корутины выполняются в тот момент, когда у них появляется такая возможность. Каждая из них выводит уникальное сообщение и, перед завершением работы, «засыпает». Только после того, как все корутины из группы завершатся, корутина main() возобновляет работу и выводит итоговое сообщение. Этот пример демонстрирует подготовку коллекции корутин и передачу их, в виде отдельных аргументов, функции gather(). Подробности о функции gather() можно найти здесь. Теперь поговорим о том, как организовать ожидание выполнения некоего условия для группы asyncio-задач. Функция asyncio.wait() позволяет организовать ожидание выполнения некоего условия для нескольких asyncio-задач. При ожидании можно ориентироваться на различные условия. Это может быть завершение выполнения всех задач, завершение выполнения одной из задач, аварийное завершение одной из задач с выдачей исключения. Познакомимся с этой функцией поближе. Функция asyncio.wait() может использоваться для организации ожидания завершения коллекции asyncio-задач. Вспомните о том, что asyncio-задача — это экземпляр класса asyncio.Task, который применяется для оборачивания корутин. Это позволяет независимо планировать выполнение корутин и выполнять их. А экземпляр класса Task даёт нам средства для работы с задачей — для запрашивания сведений о её состоянии и для получения результатов её работы. Подробности об asyncio-задачах можно найти здесь. Поговорим об использовании функции asyncio.wait(). Функция asyncio.wait() принимает коллекции объектов, допускающих ожидание. Обычно это — объекты Task. Коллекция может быть представлена списком, словарём или множеством, которые содержат заранее созданные объекты задач. Создать их можно, например, пользуясь функцией asyncio.create_task() и механизмом спискового включения: Функция asyncio.wait() не завершит работу до тех пор, пока в применении к переданной ей коллекции задач не будет выполнено некое условие. По умолчанию это условие заключается в том, чтобы выполнение всех задач было бы завершено. Функция wait() возвращает кортеж, включающий в себя два множества. Первое множество содержит все объекты задач, соответствующие условию. Второе содержит все остальные объекты задач, то есть те, которые условию не соответствуют. Эти множества называют множеством завершённых (done) задач и множеством незавершённых (pending) задач: Функция asyncio.wait() — это, с технической точки зрения — функция корутины, которая возвращает корутину. Это позволяет организовать ожидание корутины с последующим получением кортежа множеств: Условие, которое ожидает функция, можно задать с помощью аргумента return_when, который по умолчанию установлен в значение asyncio.ALL_COMPLETED. Это значение можно передать функции и в явном виде: Используя функцию asyncio.wait() можно настроить её и так, чтобы она завершалась бы при завершении одной из переданных ей задач. Делается это путём установки аргумента return_when в значение FIRST_COMPLETED: Первая завершённая задача при таком подходе попадает в группу завершённых задач, а выполнение остальных задач при этом не отменяется, они продолжают выполняться в конкурентном режиме. Функция asyncio.wait() позволяет дождаться первого сбоя задачи с выдачей исключения. Это условие задаётся путём передачи этой функции аргумента return_when со значением FIRST_EXCEPTION: В данном случае множество завершённых задач будет содержать первую задачу, которая дала сбой и вызвала исключение. Если же сбой не дала ни одна задача — множество завершённых задач будет включать в себя все задачи, а возврат из wait() произойдёт только после завершения работы всех задач. Указать то, как долго мы хотим ждать выполнения заданного условия, можно с помощью аргумента timeout, который содержит тайм-аут, выраженный в секундах. Если заданный промежуток времени истечёт до выполнения условия, возвращается кортеж задач, содержащий подмножество задач, соответствующих условию на момент возврата. Например — это может быть подмножество завершённых задач в том случае, если условием ожидания является завершение всех задач. Например: Если время выйдет до того, как будет выполнено условие, какого-либо исключения выдано не будет, а задачи, не соответствующие условию, не будут отменены. Теперь, когда мы немного разобрались с функцией asyncio.wait(), посмотрим на рабочий пример её применения. Исследуем ожидание завершения выполнения нескольких задач с использованием функции asyncio.wait(). В этом примере мы определим простую корутину, которая будет использоваться для создания задач. Эта корутина генерирует случайное значение, ненадолго приостанавливается и выводит сообщение со сгенерированным ей значением. Главная корутина создаёт на основе этой корутины несколько задач с использованием механизма спискового включения. После этого организуется ожидание выполнения всех этих задач. Вот полный код примера: При запуске этого примера сначала создаётся корутина main(), которая используется как точка входа в asyncio-программу. Затем эта корутина, пользуясь механизмом спискового включения, создаёт список из 10 задач. Каждой из этих задач передаётся уникальный аргумент от 0 до 9. Потом корутина main() приостанавливается и ожидает завершения выполнения всех задач. Задачи выполняются. Каждая из них генерирует случайное значение, на некоторое время «засыпает», а после этого выводит сгенерированное значение. После того, как все задачи завершатся, корутина main() возобновляет работу и выводит итоговое сообщение. В этом примере показано то, как функцию wait() можно использоваться для организации ожидания завершения коллекции задач. Это, вероятно, тот самый сценарий её использования, который встречается чаще всего. Обратите внимание на то, что результаты выполнения этого кода каждый раз будут разными. Это так из-за того, что в нём используется генератор случайных чисел. Вот — учебное руководство, в котором можно узнать подробности о функции wait(). Наша следующая тема — организация ожидания выполнения отдельной корутины с указанием лимита времени. Можно организовать ожидание завершения выполнения asyncio-задачи или корутины с указанием лимита времени. Делается это с помощью функции asyncio.wait_for(). Если время истечёт до завершения задачи — она отменяется. Функция asyncio.wait_for() позволяет вызывающей стороне ожидать завершения выполнения asyncio-задачи или корутины с указанием тайм-аута. Если тайм-аут не задан — эта функция будет ожидать завершения задачи. Если тайм-аут задан и время истекло до завершения выполнения задачи — задача отменяется. coroutine asyncio.wait_for(aw, timeout) Ожидает завершения выполнения объекта aw, допускающего ожидание, учитывая заданный тайм-аут timeout. Coroutines and Tasks Эта функция позволяет вызывающей стороне и формулировать свои ожидания относительно того, как долго она готова ждать завершения задачи, и принудительно завершать задачу в том случае, если вышло время, отведённое на её выполнение. Теперь, когда мы уже кое-что знаем о функции asyncio.wait_for() — поговорим о том, как ей пользоваться. Функция asyncio.wait_for() принимает объект, допускающий ожидание, и тайм-аут. Объект может быть представлен корутиной или задачей. Значение, представляющее тайм-аут, обязательно должно быть передано функции. Если временное ограничение не задаётся — её аргумент timeout нужно установить в значение None. Если ограничение задаётся — это может быть целое число или число с плавающей запятой, представляющее количество секунд. Функция wait_for() возвращает корутину, которая не выполняется до тех пор, пока вызывающая сторона не использует, явным образом, выражение await для организации ожидания её выполнения, или до тех пор, пока её выполнение не будет запланировано в виде задачи. Например: Если этой функции предали корутину — она, при выполнении корутины wait_for(), будет преобразована в задачу. Если тайм-аут истечёт до завершения задачи — задача отменяется, вызывается исключение asyncio.TimeoutError. Его может понадобиться обработать. Например: Если задача, выполнения которой мы ожидаем, аварийно завершит работу, выдав необработанное исключение, это исключение будет передано вызывающей стороне, ожидающей завершения работы корутины wait_for(). Это исключение может нуждаться в обработке: Рассмотрим пример использования функции asyncio.wait_for() с тайм-аутом Исследуем пример использования функции asyncio.wait_for() с тайм-аутом в ситуации, когда время истекает до завершения работы задачи. Здесь мы используем корутину, очень похожую на ту, которая использовалась в предыдущем подобном примере. Но в этом примере вызывающая сторона даёт корутине на выполнение лишь 0,2 секунды — или 200 миллисекунд. Напомню, что 1 секунда равна 1000 миллисекундам. Корутина, используемая для создания задачи, в сравнении с предыдущим примером, изменена. Благодаря этому она «засыпает» на время, превышающее одну секунду. Это обеспечивает то, что тайм-аут всегда истекает до завершения задачи. Вот код примера: При запуске этого кода сначала создаётся корутина main(), являющаяся точкой входа в asyncio-программу. Корутина main() создаёт задачу на основе корутины. Затем она взывает функцию wait_for(), передаёт ей задачу и задаёт тайм-аут в 0,2 секунды. Главная корутина приостанавливается и выполняется задача task_coro(). Она выводит сообщение и ненадолго «засыпает». Корутина main() возобновляет работу после того, как истечёт тайм-аут. Корутина wait_for() отменяет выполнение корутины task_coro(), а корутина main() приостанавливается. Задача task_coro() снова запускается и отвечает на запрос об остановке. Она вызывает исключение TimeoutError и завершает работу. Корутина main() возобновляет работу и обрабатывает исключение TimeoutError, вызванное задачей task_coro(). Этот пример демонстрирует использование функции wait_for() с тайм-аутом, и то, как отменяются задачи, на выполнение которых не хватает отведённого им времени. Выходные данные этой программы будут разными при каждом её запуске из-за того, что в ней используется генератор случайных чисел. Подробности о функции wait_for() можно найти здесь. Поговорим теперь о том, как защищать asyncio-задачи от отмены их выполнения. Выполнение asyncio-задачи можно отменить, вызывав её метод cancel(). Задачу можно защитить от отмены, обернув её в вызов функции asyncio.shield(). Поговорим об этой функции. Функция asyncio.shield() оборачивает объект, допускающий ожидание, в объект Future, который будет принимать запросы на отмену выполнения обёрнутого в него объекта. Защищает объект, допускающий ожидание, от отмены его выполнения. Coroutines and Tasks Это означает, что такой объект Future можно передавать задачам, которые могут попытаться отменить его выполнение. При этом запросы на отмену будут выглядеть так, будто они завершились успешно, но при этом защищённая задача или корутина продолжит выполняться. Это может оказаться полезным в asyncio-программах, в которых некоторые задачи допускается отменять, а некоторые, возможно, с более высоким приоритетом, отменять не допускается. Ещё это может пригодиться в программах, в которых некоторые задачи можно безопасно отменить, а некоторые — нет. Например — задачи, которые спроектированы с учётом особенностей модуля asyncio, можно отменять. А некоторые другие задачи, не поддерживающие безопасную досрочную отмену их выполнения, нужно защищать. Теперь, когда мы, в общих чертах, разобрались с asyncio.shield(), поговорим об использовании этой функции. Функция asyncio.shield() защищает от отмены объекты Task или корутины. Она принимает объект, допускающий ожидание, и возвращает объект asyncio.Future. Дождаться завершения работы этого объекта можно, воспользовавшись ключевым словом await. Его можно и передавать другим задачам или корутинам: Отменить выполнение возвращённого функцией объекта Future можно, вызвав его метод cancel(). Защищённая задача при этом продолжит выполняться, но запрос на отмену выполнения будет выглядеть так, будто он завершился успешно: Любые корутины, ожидающие выполнения объекта Future, выдадут исключение asyncio.CancelledError, которое может нуждаться в обработке: Тут важно помнить о том, что запрос на отмену, выполненный для объекта Future, не передаётся заключённой в него задаче. Это значит, что подобный запрос как бы «поглощается» защитным объектом. Например: Если функции asyncio.shield() передаётся корутина — она оборачивается в объект asyncio.Task() и планируется её немедленное выполнение. Это значит, что для того, чтобы внутренняя корутина могла бы запуститься, нет нужды ожидать выполнения защитного объекта Future с помощью await. awaitable asyncio.shield(aw) … Если aw — это корутина, то автоматически планируется её выполнение в виде объекта Task. Coroutines and Tasks Если защищённая задача отменяется — то запрос на её отмену распространяется и на защитный объект, выполнение которого тоже отменяется: Теперь, когда мы знаем о том, как пользоваться функцией asyncio.shield() — рассмотрим пример. Исследуем возможности функции asyncio.shield()по защите задач от отмены их выполнения. В этом примере мы определяем простую корутину, которая принимает целочисленный аргумент, на секунду «засыпает», а потом возвращает этот аргумент. Эта корутина будет использоваться для создания задачи, выполнение которой можно запланировать. Здесь мы определим и вторую корутину, которая принимает задачу, ненадолго «засыпает», а потом отменяет задачу. В главной корутине мы защитим первую задачу и передадим её второй задаче, а потом дождёмся завершения работы защищённой задачи. Мы ожидаем, что отмена выполнения коснётся лишь защитного объекта, а находящаяся у него внутри задача не пострадает. Отмена задачи нарушит работу главной корутины. Мы можем проверить состояние защищаемой задачи в конце программы. Ожидается, что её работа завершится нормально, что на неё не повлияет запрос на отмену выполнения, сделанный для защитного объекта. Вот полный код примера: Здесь сначала создаётся корутина main(), которая представляет собой точку входа в программу. Затем создаётся корутина, на основе которой создаётся объект Task, который планируется на выполнение. Эта задача защищается от отмены. Защищённая задача передаётся корутине cancel_task(). Она так же оборачивается в задачу, которая планируется на выполнение. После этого главная корутина ждёт завершения работы защищённой задачи, ожидая появления исключения CancelledError. Задача некоторое время работает, после чего «засыпает». Задача, которая отменяет другую задачу, запускается, на некоторое время «засыпает», а потом отменяет защищённую задачу. Запрос на отмену задачи завершается успешно. Всё это вызывает исключение CancelledError в защитном объекте Future, но не в защищаемой им задаче. Корутина main() возобновляет работу и реагирует на исключение CancelledError, выводя сообщение. После этого она «засыпает» на более длительное время. Потом защищённая задача возобновляет работу, она нормально завершается и возвращает значение. В итоге корутина main() возобновляет работу и выводит сведения о состоянии защитного объекта Future и защищённой им задачи. В выводе программы, представленном ниже, можно видеть, что защитный объект отменён, а включённая в него задача нормально завершила работу и возвратила значение. Этот пример демонстрирует применение функции asyncio.shield() для защиты задач от отмены их выполнения. Подробнее этот вопрос освещён здесь. Наша следующая тема посвящена запуску блокирующих задач в asyncio-программах. Мы в wunderfund.io занимаемся высокочастотной алготорговлей с 2014 года. Высокочастотная торговля — это непрерывное соревнование лучших программистов и математиков всего мира. Присоединившись к нам, вы станете частью этой увлекательной схватки. Мы предлагаем интересные и сложные задачи по анализу данных и low latency разработке для увлеченных исследователей и программистов. Гибкий график и никакой бюрократии, решения быстро принимаются и воплощаются в жизнь. Сейчас мы ищем плюсовиков, питонистов, дата-инженеров и мл-рисерчеров. Присоединяйтесь к нашей команде.