Цикл событий (Event Loop)
Цикл событий – это цикл, который может: регистрировать задачи для выполнения, выполняет их, задерживает или даже отменяет их и обрабатывать различные события, связанные с этими операциями. Обычно мы планируем несколько асинхронных функций в цикле событий. Цикл запускает одну функцию, в то время как эта функция ожидает ввода-вывода, приостанавливает ее и запускает другую. Когда первая функция завершает ввод-вывод, она возобновляется. Таким образом, две или более функции могут работать совместно. Это главная цель цикла событий.
Цикл событий также может передавать ресурсоемкие функции в пул потоков для обработки. Внутренние элементы цикла событий довольно сложны, и нам не нужно беспокоиться об этом. Нам просто нужно помнить, что цикл обработки событий – это механизм, с помощью которого мы можем планировать наши асинхронные функции и запускать их.
Futures и Tasks (Задачи)
Future – это объект, который должен иметь результат в будущем. Это такое себе обещание на получения результата выполнения, при этом может быть получен не только успешный результат, но и ошибка возникшая в результате выполнения.
Задача (Task) – это подкласс Future, который служит оберткой для корутины. Когда корутина завершается, результат Задания становится доступным.
Корутина – это способ приостановить функцию и периодически возвращать серию значений. Функция созданная как корутина может приостановить свое выполнение, используя в выражении ключевые слова yield, yield from или await (python 3.5+). Функция приостанавливается до тех пор, пока оператор yield не получит значение.
Объединение цикла событий и задач
Все предельно просто. Нам нужен цикл событий, и нам нужно зарегистрировать наши объекты Future / Task в цикле событий. Цикл будет планировать и запускать их. Мы можем добавить обратные вызовы к нашим объектам Future / Task, чтобы мы могли получать уведомления, когда появятся результаты выполнения.
Типовым примером можн считать использование корутин, которые оборачиваются Future и далее создается объект Task. Когда корутина выполняет оператор yield, она приостанавливается. Когда ей передается значение, она возобновляет свою работу. Когда кортина завершается return , задача завершена и получает результирующие значение. Любой связанный обратный вызов выполняется. Если корутина вызывает исключение, задача завершается с ошибкой.
Например
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
import asyncio @asyncio.coroutine def slow_operation(): # yield from suspends execution until # there's some result from asyncio.sleep yield from asyncio.sleep(1) # our task is done, here's the result return 'Future is done!' def got_result(future): print(future.result()) # Our main event loop loop = asyncio.get_event_loop() # We create a task from a coroutine task = loop.create_task(slow_operation()) # Please notify us when the task is complete task.add_done_callback(got_result) # The loop will close when the task has resolved loop.run_until_complete(task) |
В примере, декоратор @asyncio.coroutine – преобразует генератор в корутину. Операция loop.create_task(slow_operation()) – создает задачу из корутины созданной вызовом slow_operation. Операция task.add_done_callback(got_result) – регистрирует обратный вызов для целей обработки результатов. Метод loop.run_until_complete(task) – запускает цикл событий, регистрирует в нем задачу и ждет ее завершение. После завершения задачи цикл событий завершается.
Функция run_until_complete отличный метод для управления циклом событий. Конечно возможен и такой вариант:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
import asyncio async def slow_operation(): await asyncio.sleep(1) return 'Future is done!' def got_result(future): print(future.result()) # We have result, so let's stop loop.stop() loop = asyncio.get_event_loop() task = loop.create_task(slow_operation()) task.add_done_callback(got_result) # We run forever loop.run_forever() |
Цикл событий запускается в бесконечном режиме, но в обработчике результатов мы завершаем цикл.
Python asyncio: Future, Task and the Event Loop
Важно понимание, что в asyncio нет никакой магии при реализации неблокирующих задач. Во время реализации asyncio стоял отдельно в стандартной библиотеке, т.к. остальные модули предоставляли только блокирующую функциональность. Вы можете использовать модуль concurrent.futures для оборачивания блокирующих задач в потоки или процессы и получения Future для использования в asyncio. Несколько таких примеров доступны на GitHub.
Это, наверно, главный недостаток сейчас при использовании asyncio, однако уже есть несколько библиотек, помогающих решить эту проблему.
Интересные замечания:
Вам не нужна функция ensure_future если Вам не нужен результат и вы не хотите получать исключения возникшие во время выполнения.
Метод run_in_executor
позволяет передать Executor и таким образом контролировать количество рабочих процессов.
Сравнение Promise из JavaScript и Future/Tast из Python
Стоит сразу заметить, что Promise в JS это не то же самое, что Future в Python. Куда более эквивалентны Promise в JS и Task в Python. При этом стоит понимать, что задача (Task) в python, состоит из Future и Corutine. Другими словами задача состоит из обещания вернуть какое-то значение и кода который ее выполнит. При этом главное отличие Future от Promise из JS, состоит в том, что Promise автоматически выполнится асинхронно, в то время как Future не выполняется вообще это просто контейнер в который значение будет помещено некоторым внешним кодом через метод set_result(), при этом Future сразу же вызовет зарегистрированные обратные вызовы (вызов регистрируются через метод add_done_callback())
В то время как Corutine основанная на тех же принципах, что и генераторы. Корутина вызывается из цикла обработки событий и возвращает объекты Future и при этом ожидает когда это событие будет завершено. После завершения в корутину может быть передано значение и получено из нее новый Future. (по анологии с циклом for и генератором который выбрасывет через yield значения).
Выражение await практически идентично yield from, поэтому, ожидая другую сопрограмму, вы останавливаетесь, пока у этой сопрограммы не будет разрешен Future, и вы не получаете возвращаемое значение. Future является повторяемым в один такт, и его итератор возвращает фактическое Future – это примерно означает, что await future равно yield from future и yield future.
Task это Future, который фактически был запущен для вычисления и привязан к циклу событий. Так что это особый вид Future (класс Task является производным от класса Future), который связан с некоторым циклом событий и имеет некоторую корутину (сопрограмму), которая служит исполнителем Task.
Задача обычно создается объектом цикла события: вы предоставляете корутину (сопрограмму) циклу, она создает объект задачи и начинает выполнять итерацию по этой корутине, как описано выше. Как только корутина закончена, Future задачи разрешается (получает результат) с помощью возвращаемого значения корутины.
Видите ли, задача очень похожа на JS Promise – она инкапсулирует фоновое задание и его результат.
Coroutine func – это фабрика корутин (сопрограмм), подобная функции генератора для генераторов. Обратите внимание на разницу между функцией корутин Python и асинхронной функцией Javascript – при вызове асинхронная функция JS создает Promise, и ее внутренний генератор немедленно начинает выполнятся, в то время как корутина Python ничего не делает, пока на ней не будет создана задача.
В Python весь код которому нужна какая-либо функция asyncio, должен всегда работает цикл обработки событий.
Однако, смешивать синхронный и асинхронный код довольно сложно – вся ваша программа должна быть асинхронной (но вы можете запускать куски синхронного кода в отдельных потоках через API asyncio threadpool API)