Корутины python что это
Перейти к содержимому

Корутины python что это

  • автор:

Короутины (coroutine)

Вопросы, касающиеся короутин (coroutine) и asyncio, относятся к этому разделу.

Это содержание было взято непосредственно из документации и унаследованно от discord.py . Скорее всего, в будущем оно будет переписано.

Что такое coroutine?​

Сoroutine это функция, которая должна быть вызвана с помощью await или yield from . Когда Python сталкивается с await , он останавливает выполнение функции в этот момент и работает над другими вещами, пока не вернется к этой точке и не завершит свою работу. Это позволяет вашей программе выполнять несколько задач одновременно без использования потоков или сложной многопроцессорной обработки.

Где я могу использовать await ?​

Вы можете использовать await только в async def функциях и нигде больше.

Что означает «blocking»?​

В асинхронном программировании блокирующий вызов — это, по сути, все части функции, которые не являются await . Однако не отчаивайтесь, потому что не все формы блокировки плохи! Использование блокирующих вызовов неизбежно, но вы должны работать над тем, чтобы убедиться, что вы не чрезмерно блокируете функции. Помните, что если вы блокируете слишком долго, то ваш бот зависнет, так как в этот момент он не остановил выполнение функции для выполнения других действий.

Если ведение журнала включено, эта библиотека попытается предупредить вас о том, что происходит блокировка, с сообщением: Heartbeat blocked for more than N seconds. См. Раздел Настройка Ведения Журнала для получения подробной информации о включении ведения журнала.

Распространенным источником слишком длительной блокировки является что-то вроде time.sleep . Не делай этого. Используйте asyncio.sleep вместо этого. Аналогично этому примеру:

# Плохо time.sleep(10)  # Хорошо await asyncio.sleep(10) 

Другим распространенным источником слишком длительной блокировки является использование HTTP-запросов с известным модулем Requests: HTTP for Humans™. В то время как Requests: HTTP for Humans™ — это удивительный модуль для неасинхронного программирования, он не является хорошим выбором для asyncio , потому что некоторые запросы могут блокировать цикл событий слишком долго. Вместо этого используйте библиотеку aiohttp , которая уже установлена с disnake.

Рассмотрим следующий пример:

# Плохо r = requests.get("http://aws.random.cat/meow") if r.status_code == 200:  json = r.json() await channel.send(json["file"])  # Хорошо async with aiohttp.ClientSession() as session: async with session.get("http://aws.random.cat/meow") as r: if r.status == 200:  json = await r.json() await channel.send(json["file"]) 

Корутины и задачи

В этом разделе приведено высокоуровневое API asyncio для работы с корутинами и задачами.

  • Корутины
  • Ожидаемые объекты
  • Запуск asyncio программы
  • Создание задач
  • Сон
  • Конкурентный запуск задач
  • Защита от отмены
  • Таймауты
  • Примитивы ожидания
  • Планирование из других потоков
  • Интроспекция
  • Объект задачи
  • Основанные на генераторах корутины

Корутины

Корутины , объявляемые с помощью async/await синтаксиса, является предпочтительным способом написания asyncio приложений. Например, следующий фрагмент кода (требует Python 3.7) напечатает «hello», ожидает 1 секунду, а затем печатает «world»:

>>> import asyncio >>> async def main(): . print('hello') . await asyncio.sleep(1) . print('world') >>> asyncio.run(main()) hello world 

Заметим, что простой вызов корутины не приведёт к её выполнению:

>>> main()

Чтобы фактически запустить корутину, asyncio предоставляет три основных механизма:

  • Функция asyncio.run() для запуска функции точки входа верхнего уровня «main()» (см. приведённый пример выше)
  • Ожидающая корутина. Следующий фрагмент кода напечатает «hello» после ожидания в 1 секунду, а затем напечатает «world» после ожидания в течении ещё 2х секунд

import asyncio import time async def say_after(delay, what): await asyncio.sleep(delay) print(what) async def main(): print(f"started at time.strftime('%X')>") await say_after(1, 'hello') await say_after(2, 'world') print(f"finished at time.strftime('%X')>") asyncio.run(main()) 

Ожидаемый вывод:

started at 17:13:52 hello world finished at 17:13:55 
async def main(): task1 = asyncio.create_task( say_after(1, 'hello')) task2 = asyncio.create_task( say_after(2, 'world')) print(f"started at time.strftime('%X')>") # Подождать, пока обе задачи не будут выполнены (должны принять # около 2 секунд.) await task1 await task2 print(f"finished at time.strftime('%X')>") 

Обратите внимание, что ожидаемые выходные данные теперь показывают, что фрагмент выполняется на 1 секунду быстрее, чем ранее:

started at 17:14:32 hello world finished at 17:14:34 

Ожидаемые объекты

Мы говорим, что объект является ожидаемым объектом, если его можно использовать в await выражении. Многие API-интерфейсы asyncio предназначены для приёма ожидаемых. Существует три основных типа ожидаемых объектов: корутины, задачи и футуры.

Python корутины являются ожидаемыми и поэтому могут ожидаться из других корутин:

import asyncio async def nested(): return 42 async def main(): # Ничего не произойдет, если мы просто вызовем "nested()". # Объект корутины создан, но не await, # так что *не будет работать вообще*. nested() # Давайте сделаем это по-другому и подождём: print(await nested()) # Напечатает "42". asyncio.run(main()) 

В этой документации термин «корутина» может использоваться для двух тесно связанных понятий:

  • Функция корутина: функция async def ;
  • Объект корутины: возвращенный объект после вызова функции корутины.

asyncio также поддерживает устаревшие основанные на генераторах корутины.

Задачи используются для конкурентного планирования корутин.

Когда корутина обвёрнута в Задачу с такими функциями, как asyncio.create_task() , то автоматически планируется запуск корутины в ближайшее время:

import asyncio async def nested(): return 42 async def main(): # Запланировать nested() ближайший одновременный запуск # с "main()". task = asyncio.create_task(nested()) # "task" теперь может использовать отмену "nested()" или # можно просто ждать, пока она не завершится: await task asyncio.run(main()) 

Объект Future — это специальный ожидаемый (await) объект низкого уровня , представляющий конечный результат асинхронной операции.

Когда объект Футуры ожидается, это означает, что корутина будет ждать, пока Футура будет решена в каком-то другом месте.

Объекты Футуры в asyncio нужны, чтобы позволить основанному на колбэках коду использоваться с async/await.

Обычно нет нужды создавать объекты Футуры на уровне кода приложения.

Объекты Футуры, иногда раскрываемые библиотеками и некоторыми asyncio API, могут быть ожидаемыми:

async def main(): await function_that_returns_a_future_object() # это также верно: await asyncio.gather( function_that_returns_a_future_object(), some_python_coroutine() ) 

Хорошим примером низкоуровневой функции, возвращающей объект Футуры, является loop.run_in_executor() .

Запуск asyncio программы

asyncio. run ( coro, *, debug=False )

Выполняет корутину coro и возвращает результат.

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

Функция не может быть вызвана, когда в том же потоке выполняется другой asyncio событийный цикл.

Если debug — True , событийный цикл будет выполняться в режиме отладки.

Функция всегда создаёт новый событийный цикл и закрывает его в конце. Его следует использовать в качестве основной точки входа для asyncio программ, и в идеале его следует вызывать только один раз.

async def main(): await asyncio.sleep(1) print('hello') asyncio.run(main()) 

Добавлено в версии 3.7.

Исходный код asyncio.run() можно найти в Lib/asyncio/runners.py.

Создание задач

asyncio. create_task ( coro, *, name=None )

Обёртывание coro корутины в Task и запланировать её выполнение. Возвращает объект задачи.

Если name не None , он задаётся как имя задачи с помощью Task.set_name() .

Задача выполняется в цикле, возвращенного get_running_loop() . Вызывает RuntimeError , если в текущем потоке нет запущенного цикла.

Функция была добавлена в Python 3.7. Ранее Python 3.7 вместо неё можно использовать низкоуровневую функцию asyncio.ensure_future() :

async def coro(): . # В Python 3.7+ task = asyncio.create_task(coro()) . # Это работает во всех версиях Python, но менее читабельно task = asyncio.ensure_future(coro()) . 

Добавлено в версии 3.7.

Изменено в версии 3.8: Добавлен параметр name .

Сон

coroutine asyncio. sleep ( delay, result=None, *, loop=None )

Блокировка на delay секунд.

Если result предоставляется, он возвращается вызывающему после завершения корутины.

sleep() всегда приостанавливает выполнение текущей задачи, позволяя выполнять другие задачи.

Устарело с версии 3.8, будет удалено в 3.10 версии.: Параметр loop.

Пример корутины, отображающей текущую дату каждую секунду в течение 5 секунд:

import asyncio import datetime async def display_date(): loop = asyncio.get_running_loop() end_time = loop.time() + 5.0 while True: print(datetime.datetime.now()) if (loop.time() + 1.0) >= end_time: break await asyncio.sleep(1) asyncio.run(display_date()) 

Конкурентный запуск задач

awaitable asyncio. gather ( *aws, loop=None, return_exceptions=False )

Запускает ожидаемые объекты в последовательности aws конкурентно.

Если какой-либо ожидаемый объект в aws является корутиной, он автоматически назначается как задача.

Если все await объекты выполнены успешно, результатом является сводный список возвращенных значений. Порядок значений результата соответствует порядку await в aws.

Если return_exceptions является False (по умолчанию), первое вызванное исключение немедленно распространяется на задачу, которая ожидает на gather() . Другие await объекты в aws последовательности не будут отменены и продолжат работу.

При return_exceptions True исключения обрабатываются так же, как и успешные результаты, и агрегируются в списке результатов.

Если gather() отменён, все представленные ожидаемые (которые ещё не завершены) также будут отменены.

Если какая-либо задача или футура в последовательности aws отменена, это рассматривается, как будто сработало исключение CancelledError — вызов gather() не отменяется в этом случае. Это необходимо для предотвращения отмены одной отправленной задачи/футуры, чтобы привести к отмене других задач/футур.

Устарело с версии 3.8, будет удалено в 3.10 версии.: Параметр loop.

import asyncio async def factorial(name, number): f = 1 for i in range(2, number + 1): print(f"Task name>: Compute factorial(i>). ") await asyncio.sleep(1) f *= i print(f"Task name>: factorial(number>) = f>") async def main(): # Запланировать дерево вызовов *конкурентно*: await asyncio.gather( factorial("A", 2), factorial("B", 3), factorial("C", 4), ) asyncio.run(main()) # Ожидаемый вывод: # # Task A: Compute factorial(2). # Task B: Compute factorial(2). # Task C: Compute factorial(2). # Task A: factorial(2) = 2 # Task B: Compute factorial(3). # Task C: Compute factorial(3). # Task B: factorial(3) = 6 # Task C: Compute factorial(4). # Task C: factorial(4) = 24 

Если return_exceptions содержит значение False, отмена gather() после того, как он был помечен как выполненный, не отменит ни одного отправленного ожидаемого объекта. Например, gather может быть помечена как выполненная после передачи исключения вызывающей стороне, поэтому вызов gather.cancel() после перехвата исключения (вызванного одним из ожидаемых объектов) из gather не отменяет другие ожидаемые объекты.

Изменено в версии 3.7: Если gather отменяется, отмена распространяется независимо от return_exceptions.

Защита от отмены

awaitable asyncio. shield ( aw, *, loop=None )

Если aw корутина, она автоматически назначается как задача.

res = await shield(something()) 
res = await something() 

кроме того, что если корутина, содержащая её, отменяется, задача, выполняемая в something() , не отменяется. С точки зрения something() отмены не произошло. Хотя его вызывающий объект всё ещё отменён, «await» выражение по-прежнему вызывает CancelledError .

Если something() отменяется другими средствами (т.е. изнутри), которые также отменяют shield() .

Если требуется полностью игнорировать отмену (не рекомендуется), функция shield() должна быть объединена с предложением try/except следующим образом:

try: res = await shield(something()) except CancelledError: res = None 

Устарело с версии 3.8, будет удалено в 3.10 версии.: Параметр loop.

Таймауты

coroutine asyncio. wait_for ( aw, timeout, *, loop=None )

Дождаться завершения aw ожидаемого с таймаутом.

Если aw корутина, она автоматически назначается как задача.

timeout может быть либо None , либо числом секунд ожидания с плавающей запятой, либо int числом. Если timeout None , блокировать до завершения футуры.

Если завершается таймаут, задача отменяется и вызывается asyncio.TimeoutError .

Чтобы избежать отмены задачи, оберните её в shield() .

Функция будет ждать, пока футура будет фактически отменена, поэтому общее время ожидания может превысить timeout.

Если ожидание отменяется, то также отменяется и будущий aw.

Устарело с версии 3.8, будет удалено в 3.10 версии.: Параметр loop.

async def eternity(): # Спать в течение одного часа await asyncio.sleep(3600) print('yay!') async def main(): # Ожидать не более 1 секунды try: await asyncio.wait_for(eternity(), timeout=1.0) except asyncio.TimeoutError: print('timeout!') asyncio.run(main()) # Ожидаемый вывод: # # timeout! 

Изменено в версии 3.7: Когда aw отменяется из-за тайм-аута, wait_for ожидает отмены aw. Ранее она сразу вызывала asyncio.TimeoutError .

Примитивы ожидания

coroutine asyncio. wait ( aws, *, loop=None, timeout=None, return_when=ALL_COMPLETED )

Конкурентный запуск ожидаемых объектов в итерации aws и блокировка, пока не будет выполнено условие, указанное в return_when.

Возвращает два множества задачи/футуры: (done, pending) .

done, pending = await asyncio.wait(aws) 

timeout (float или int), если он указан, можно использовать для управления максимальным количеством секунд ожидания перед возвращением.

Обратите внимание, что функция не вызывает asyncio.TimeoutError . Футуры или задачи, которые не были выполнены при наступлении тайм-аута, просто возвращаются во втором множестве.

return_when указывает, когда функция должна возвращать. Она должна быть одной из следующих констант:

Константа Описание
FIRST_COMPLETED Функция возвращает, когда любая футура завершится или отменится.
FIRST_EXCEPTION Функция возвращает после завершения любого процесса футуры путём создания исключения. Если в футуре исключение не вызывается, то оно эквивалентно ALL_COMPLETED .
ALL_COMPLETED Функция возвращает после завершения или отмены всех футур.

В отличие от wait_for() , wait() не отменяет футуры при наступлении тайм-аута.

Не рекомендуется, начиная с версии 3.8: Если какой-либо ожидаемый в aws является корутиной, он автоматически назначается как задача. Непосредственная передача объектов корутине в wait() является устаревшей практикой, т. к. приводит к запутанному поведению .

Устарело с версии 3.8, будет удалено в 3.10 версии.: Параметр loop.

wait() автоматическое планирование корутины как задачи, затем возвращает создаваемые объекты задачи в множестве (done, pending) . Поэтому следующий код не будет работать так, как ожидалось:

async def foo(): return 42 coro = foo() done, pending = await asyncio.wait(coro>) if coro in done: # Ветка никогда не будет запущена! 

Вот как можно зафиксировать вышеуказанный фрагмент:

async def foo(): return 42 task = asyncio.create_task(foo()) done, pending = await asyncio.wait(task>) if task in done: # Теперь все будет работать так, как и ожидалось. 

Не рекомендуется, начиная с версии 3.8: Передача корутиновых объектов непосредственно в wait() устарела.

asyncio. as_completed ( aws, *, loop=None, timeout=None )

Запуск ожидаемых объектов в aws итеративно и конкурентно. Возвращает итератор корутины. Каждую возвращенную корутину можно ожидать, чтобы получить самый ранний следующий результат от итерируемого из оставшихся ожидаемых.

Вызывает asyncio.TimeoutError , если тайм-аут наступает до выполнения всех футур.

Устарело с версии 3.8, будет удалено в 3.10 версии.: Параметр loop.

for coro in as_completed(aws): earliest_result = await coro # . 

Планирование из других потоков

asyncio. run_coroutine_threadsafe ( coro, loop )

Отправить корутину в событийный цикл. Потокобезопасный.

Возвращает concurrent.futures.Future дожидаясь результата из другого потока ОС.

Функция вызывается из потока операционной системы, отличного от того, где выполняется событийный цикл. Пример:

# Создание корутины coro = asyncio.sleep(1, result=3) # Отправить корутину в заданный цикл future = asyncio.run_coroutine_threadsafe(coro, loop) # Ожидать результата с необязательным аргументом timeout assert future.result(timeout) == 3 

Если в корутине вызывается исключение, возвращенная футура будет уведомлена. Также можно использовать отмену задачи в событийном цикле:

try: result = future.result(timeout) except asyncio.TimeoutError: print('The coroutine took too long, cancelling the task. ') future.cancel() except Exception as exc: print(f'The coroutine raised an exception: exc!r>') else: print(f'The coroutine returned: result!r>') 

В отличие от других asyncio функций данная функция требует явной передачи loop аргумента.

Добавлено в версии 3.5.1.

Интроспекция

asyncio. current_task ( loop=None )

Возвращает текущую Task сущность или None , если задача не выполняется.

Если loop — None , используется get_running_loop() для получения текущего цикла.

Добавлено в версии 3.7.

asyncio. all_tasks ( loop=None )

Возвращает множество ещё не завершенных объектов Task , запущенных в цикле.

Если loop — None , используется get_running_loop() для получения текущего цикла.

Добавлено в версии 3.7.

Объект задачи

class asyncio. Task ( coro, *, loop=None, name=None )

Футуроподобный объект, запускающий Python корутину . Не потокобезопасной.

Задачи используются для запуска корутин в событийных циклах. Если корутина ожидает футуры, задача приостанавливает выполнение корутины и ожидает завершения футуры. Когда выполнено, футура возобновляет исполнение обёрнутой корутины.

Событийный цикл использует совместное планирование: событийный цикл выполняет одну задачу одновременно. В то время как задача ожидает для завершения футуры, событийный цикл выполняет другие задачи, колбэки или выполняет операции ввода-вывода.

Используйте высокоуровневую функцию asyncio.create_task() , чтобы создать задачи или низкоуровневые функции loop.create_task() или ensure_future() . Не рекомендуется создавать экземпляры задач вручную.

Для отмены выполняемой задачи используйте метод cancel() . Этот вызов приведёт к тому, что задача бросит CancelledError исключение в обернутую корутину. Если корутина ожидает в объекте футуры во время отмены, объект футуры будет отменён.

Можно использовать cancelled() , чтобы проверить, была ли задача отменена. Метод возвращает True , если обёрнутая корутина не подавила CancelledError исключение и фактически была отменена.

Задачи поддерживают модуль contextvars . При создании задачи она копирует текущий контекст, а затем запускает корутину в скопированном контексте.

Изменено в версии 3.7: Добавлена поддержка модуля contextvars .

Изменено в версии 3.8: Добавлен параметр name .

Устарело с версии 3.8, будет удалено в 3.10 версии.: Параметр loop.

Запрос отмены задачи.

Это позволяет создать CancelledError исключение для обернутой корутины на следующем цикле событийного цикла.

После этого у корутины есть шанс очистить или даже отклонить просьбу, подавив исключение в блоке try … except CancelledError … finally . Поэтому, в отличие от Future.cancel() , Task.cancel() не гарантирует, что задача будет отменена, хотя подавление отмены полностью не распространено и активно отговаривается.

В следующем примере показано, как корутины могут перехватывать запрос на отмену:

async def cancel_me(): print('cancel_me(): before sleep') try: # Ждать 1 секунду await asyncio.sleep(3600) except asyncio.CancelledError: print('cancel_me(): cancel sleep') raise finally: print('cancel_me(): after sleep') async def main(): # Создание задачи "cancel_me" task = asyncio.create_task(cancel_me()) # Ждать 1 секунду await asyncio.sleep(1) task.cancel() try: await task except asyncio.CancelledError: print("main(): cancel_me is cancelled now") asyncio.run(main()) # Ожидаемый результат: # # cancel_me(): before sleep # cancel_me(): cancel sleep # cancel_me(): after sleep # main(): cancel_me is cancelled now 

Возвращает True , если задача отменена.

Задача отменена, когда запрашивалась отмена с cancel() и обернутая корутина распространила в неё CancelledError исключение.

Возвращает True , если задача завершена.

Задача завершена, когда обернутая корутина либо возвратила значение, либо вызвала исключение, либо задача была отменена.

Возвращает результат выполнения задачи.

Если задача является завершенной, то результатом обернутой корутины является возвращаемое (или если корутина вызвала исключение, то это исключение возникает повторно.)

Если задача была отменена, это метод вызывает CancelledError исключение.

Если результат задачи ещё не доступен, это метод вызывает InvalidStateError исключение.

Возвращает исключение задачи.

Если у обернутой корутины возникло исключение, то возвращается это исключение. Если обернутая корутина возвращается нормально, то этот метод возвращает None .

Если задача была отменена, это метод вызывает CancelledError исключение.

Если задача еще не завершена, это метод вызывает InvalidStateError исключение.

add_done_callback ( callback, *, context=None )

Добавление колбэка для выполнения при выполнении задачи.

Этот метод должен быть использован только в низкоуровневом основанном на колбэках коде.

Для получения дополнительной информации см. документацию Future.add_done_callback() .

Удалить callback из списка колбэков.

Этот метод должен быть использован только в низкоуровневом основанном на колбэках коде.

Для получения дополнительной информации см. документацию Future.remove_done_callback() .

Возвращает список фреймов стека для этой задачи.

Если обёрнутая корутина не выполнена, он возвращает стек, где он приостановлен. Если корутина успешно завершена или отменена, возвращает пустой список. Если корутина была прервана исключением, возвращает список кадров трейсбэка.

Фреймы всегда упорядочиваются от самых старых до самых новых.

Для приостановленной корутины возвращается только одни фрейм стека.

Необязательный аргумент limit устанавливает максимальное количество возвращаемых кадров; по умолчанию возвращаются все доступные фреймы. Порядок возвращаемого списка различается в зависимости от того, возвращается ли стек или трассировка: возвращаются самые новые фреймы стека, но возвращаются самые старые фреймы трассировки. (Это соответствует поведению модуля трассировки.)

Печать стека или трейсбэка для этой задачи.

При этом выводятся выходные данные, аналогичные данным модуля traceback для фреймов, извлекаемых get_stack() .

Аргумент limit передается непосредственно get_stack() .

Аргумент file представляет собой поток I/O, в который записываются выходные данные; по умолчанию выходные данные записываются в sys.stderr .

Возвращает объект корутины, обернутый Task .

Добавлено в версии 3.8.

Возвращает имя задачи.

Если ни одно имя не было явно назначено задаче, реализация задачи asyncio по умолчанию создаёт имя по умолчанию во время создания экземпляра.

Добавлено в версии 3.8.

set_name ( value )

Задание имя задачи.

Аргументом value может быть любой объект, который затем преобразуется в строку.

В реализации задачи по умолчанию имя будет отображаться в repr() выходных данных объекта задачи.

Добавлено в версии 3.8.

classmethod all_tasks ( loop=None )

Возвращает множество всех задач для событийного цикла.

По умолчанию возвращаются все задачи для текущего событийного цикла. Если loop None , то используется get_event_loop() функция для получения текущего цикла.

Устарело с версии 3.7, будет удалено в 3.9 версии.: Не вызывайте этот метод задач. Вместо этого используйте функцию asyncio.all_tasks() .

classmethod current_task ( loop=None )

Возвращает текущую запущенную задачу или None .

Если loop — None , используется функция get_event_loop() для получения текущего цикла.

Устарело с версии 3.7, будет удалено в 3.9 версии.: Не вызывайте этот метод задач. Вместо него используйте функцию asyncio.current_task() .

Основанные на генераторах корутины

Поддержка основанных на генераторах корутин запрещено и планируется к удалению в Python 3.10.

Корутины на основе генератора предшествовали синтаксису async/await. Они представляют собой Python генераторы, которые используют yield from выражения для ожидания футур и других корутин.

Генераторные корутины должны быть задекорированы @asyncio.coroutine , хотя это не применяется.

Декоратор для маркировки основанных на генераторах корутин.

Этот декоратор обеспечивает совместимость устаревших основанных на генераторах корутин с async/await кодом:

@asyncio.coroutine def old_style_coroutine(): yield from asyncio.sleep(1) async def main(): await old_style_coroutine() 

Этот декоратор не должен использоваться для async def корутин.

Устарело с версии 3.8, будет удалено в 3.10 версии.: Используйте async def вместо этого.

asyncio. iscoroutine ( obj )

Метод отличается от inspect.iscoroutine() потому что возвращает True для основанных на генераторах корутин.

asyncio. iscoroutinefunction ( func )

Метод отличается от inspect.iscoroutinefunction() , потому что возвращает True для основанных на генераторах функций корутин декорированных с @coroutine .

Пишем нашу первую сопрограмму

В этой статье мы вместе с вами создадим сопрограмму на Python. Но перед этим вы выясните что такое корутины, для чего они нужны, какие языки программирования поддерживают их и как с ними работать. Также вы узнаете, чем корутины и потоки отличаются друг от друга и познакомитесь с недостатками сопрограмм.

Асинхронное программирование — это важная часть разработки приложений, которая позволяет выполнять несколько задач одновременно, не блокируя основной поток.

Асинхронность напоминает чемпионат по бальным танцам, где под одну музыку множество пар танцуют свой собственный танец.

Асинхронный код сложный, запутанный, требует много обратных вызовов (callback) и обработки ошибок. Чтобы сделать его сделать более читаемым и понятным используют корутины. Они позволяют писать асинхронный код в синхронном стиле, без необходимости использовать сложные конструкции, такие как промисы (promise) или фьючерсы (future).

Что такое корутины

Корутины или сопрограммы (англ. coroutine) — это специальные функции, которые могут приостанавливать свое выполнение и передавать управление другим корутинам, а затем продолжать с того места, где остановились.

Что могут корутины?

  • Иметь несколько точек входа и выхода: в отличие от обычных подпрограмм, которые имеют одну точку входа и одну точку выхода.
  • Приостанавливать свое выполнение в любой момент: с помощью специального оператора (например, yield в Python или await в Kotlin), сохраняя свое состояние (локальные переменные и стек вызовов).
  • Возобновить свое выполнение с того же места: по запросу другой корутины или внешнего кода.
  • Работать кооперативно: они добровольно отдают управление друг другу, а не конкурируют за ресурсы.
  • Не привязываться к определенному системному потоку (thread), а выполняться поверх них: это означает, что один поток может запускать несколько корутин параллельно, переключаясь между ними по мере необходимости.

Какие языки программирования поддерживают корутины

Корутины не являются новой концепцией в программировании. Они были предложены еще в 1958 году Мелвином Конвеем (Melvin Conway) и использовались в различных языках программирования с тех пор.

Некоторые языки поддерживают корутины на уровне языка, то есть предоставляют специальные ключевые слова или конструкции для работы с ними:

  • Python имеет ключевые слова async и await для объявления и использования корутин.
  • В Kotlin есть ключевое слово suspend для определения функций, которые могут быть вызваны из корутин.
  • В Lua предусмотрены функции coroutine.create и coroutine.resume для создания и запуска корутин.

Другие языки поддерживают корутины на уровне библиотеки, то есть предоставляют специальные классы или функции для работы с ними:

  • C# имеет класс System.Threading.Tasks.Task для представления асинхронных операций, которые могут быть запущены как корутины.
  • Java имеет библиотеку java.util.concurrent.CompletableFuture для создания и комбинирования асинхронных задач.
  • JavaScript имеет объект Promise для оборачивания асинхронных операций в цепочки обратных вызовов.

Код с обратными вызовами VS с корутинами

Код с обратными вызовами

Если мы захотим загрузить данные из сети или из файла, то мы не сможем делать это в основном потоке программы, так как это заблокирует его и сделает наше приложение нереактивным. Вместо этого нам придется запустить этот процесс в фоновом потоке и дождаться его завершения. Минусы такого подхода:

  • Работа с фоновыми потоками сложна и подвержена ошибкам: нужно управлять жизненным циклом потоков, синхронизировать доступ к общим ресурсам и обрабатывать исключения
  • Асинхронный код с обратными вызовами трудно читать и понимать, особенно если он имеет много вложенных вызовов.

Пример кода с callback:

# Асинхронный код с обратными вызовами def load_data (url, callback): # Загрузить данные из url в фоновом потоке # После загрузки вызвать callback c результатом def process_data (data): # Обработать данные def display_data (data): # Отобразить данные на экране load_data("https://example.com ", lambda data: process_data(data)) display_data(data) 

Код с корутинами

Корутины упрощают асинхронный код, позволяя нам писать его в синхронном стиле, как будто мы выполняем обычную последовательность действий:

# Асинхронный код с корутинами async def load_data(url): # Загрузить данные из url в фоновом потоке # Приостановить выполнение корутины до получения результата # Вернуть результат async def process_data(data): # Обработать данные async def display_data(data): # Отобразить данные на экране data = await load_data("https://example.com ") data = await process_data(data) await display_data(data) 

Как видим, код с coroutine выглядит более линейным и понятным, чем код с обратными вызовами. Корутины также позволяют нам легко комбинировать и организовывать асинхронные задачи, например, запускать их параллельно или последовательно, ждать их завершения или отмены.

Еще один пример работы сопрограммы

Для того, чтобы создать сопрограмму на Python, нужно определить функцию с ключевым словом async. Это означает, что функция может быть вызвана из другой сопрограммы с помощью оператора await, который приостанавливает выполнение текущей сопрограммы и ждет результата вызванной.

Давайте напишем сопрограмму, которая делает запрос к сайту Bing и возвращает полученный HTML-код:

# Импортируем библиотеку для работы с HTTP-запросами import aiohttp # Определяем сопрограмму для запроса к Bing async def get_bing_html(): # Создаем асинхронный HTTP-клиент async with aiohttp.ClientSession() as session: # Делаем GET-запрос к Bing async with session.get("https://www.bing.com ") as response: # Читаем ответ как текст html = await response.text() # Возвращаем HTML-код return html 

Для того, чтобы запустить эту сопрограмму, нам нужно использовать специальную функцию asyncio.run(), которая принимает сопрограмму в качестве аргумента и запускает ее в цикле событий (event loop).

Цикл событий — это механизм, который управляет выполнением сопрограмм и других асинхронных операций, таких как ввод-вывод или таймеры. Цикл событий постоянно проверяет, есть ли какие-то готовые к выполнению или завершенные задачи, и переключает между ними по мере необходимости.

# Импортируем библиотеку для работы с асинхронностью import asyncio # Запускаем сопрограмму для запроса к Bing bing_html = asyncio.run(get_bing_html()) # Печатаем длину полученного HTML-кода print(len(bing_html)) 

Когда мы запустим этот код, то увидим, что он работает асинхронно и не блокирует основной поток программы. Более того мы можем делать другие действия, пока ждем ответа от Bing, а также запускать несколько сопрограмм одновременно и ждать их завершения в любом порядке. Давайте напишем еще одну сопрограмму, которая делает запрос к Google и возвращает полученный HTML-код:

# Определяем сопрограмму для запроса к Google async def get_google_html(): # Создаем асинхронный HTTP-клиент async with aiohttp.ClientSession() as session: # Читаем ответ как текст html = await response.text() # Возвращаем HTML-код return html 

Теперь мы можем запустить обе сопрограммы параллельно и ждать их результатов:

# Запускаем обе сопрограммы параллельно и ждем их результатов bing_html, google_html = asyncio.run(asyncio.gather( get_bing_html(), get_google_html() )) # Печатаем длины полученных HTML-кодов print(len(bing_html)) print(len(google_html)) 

Оба запроса выполняются одновременно и занимают меньше времени, чем если бы мы делали их последовательно. Это показывает, что сопрограммы позволяют нам эффективно использовать ресурсы и ускорить наш код.

Различия между корутинами и потоками

Корутины и потоки — это два разных способа организации параллельного и асинхронного выполнения кода. Они имеют свои сходства и отличия, которые влияют на производительность, память, сложность и надежность программ. Вот некоторые ключевые различия между корутинами и потоками:

  • Потоки — это сущности, которые управляются операционной системой
  • Корутины — сущности, которые управляются языком программирования или библиотекой
  • Потоки имеют свой собственный стек памяти
  • Корутины используют общий стек памяти
  • Потоки переключаются между собой прерывисто (preemptively), то есть операционная система может прервать выполнение одного потока и запустить другой в любой момент
  • Корутины переключаются между собой согласованно (cooperatively), то есть программа или язык определяет точки, в которых корутина может приостановиться и передать управление другой
  • Потоки могут выполняться параллельно на нескольких процессорах или ядрах
  • Корутины выполняются последовательно в рамках одного потока
  • Потоки требуют синхронизации доступа к общим данным с помощью механизмов блокировки, таких как мьютексы (mutex) или семафоры (semaphore)
  • Корутины не требуют блокировки данных, так как они не выполняются одновременно

И наконец — потоки требуют больше ресурсов для создания, уничтожения и переключения, чем корутины.

Преимущества использования корутин

Применение сопрограмм имеет ряд преимуществ по сравнению с использованием потоков. Корутины:

  • Позволяют писать асинхронный код в синхронном стиле, что упрощает чтение и понимание кода.
  • Избавляют от необходимости использовать сложные конструкции, такие как обратные вызовы (callback), промисы (promise) или фьючерсы (future).
  • Экономят память и время, так как они не создают свой собственный стек и не требуют переключения контекста между собой. Это позволяет запускать большое количество корутин на одном потоке без значительных потерь производительности.
  • Поддерживают структурированную параллельность (structured concurrency), то есть они работают в рамках определенной области видимости (scope). Это помогает избежать утечек памяти и ошибок жизненного цикла, так как корутины автоматически отменяются при выходе из своей области видимости.
  • Интегрируются с многими библиотеками и фреймворками для Android, такими как Jetpack, Retrofit и Room. Это позволяет использовать сопрограммы для работы с сетью, базами данных, пользовательским интерфейсом и другими асинхронными операциями.

Недостатки сопрограмм

  • Требуют специального синтаксиса, такого как ключевые слова async и await, для объявления и использования сопрограмм. Такой код не может быть смешан с синхронным кодом без дополнительных преобразований.
  • Не могут выполняться параллельно на нескольких процессорах или ядрах, так как они работают в рамках одного потока. Это означает, что корутины не могут использоваться для задач, которые требуют высокой вычислительной мощности или распределенной обработки.
  • Могут быть сложными для отладки и тестирования, так как они могут приводить к неожиданным результатам из-за асинхронности и кооперативности. Например, корутина может быть отменена в середине выполнения или продолжить выполнение после долгого простоя. Для облегчения отладки и тестирования корутин можно использовать специальные инструменты, такие как Coroutine Debugger или TestCoroutineDispatcher.

В заключение, мы можем сказать, что корутины — это полезный и мощный инструмент для асинхронного программирования, который позволяет писать код в синхронном стиле, экономить ресурсы и поддерживать структурированную параллельность.

Генераторы и корутины в Python

В предыдущей статье Итерируемые объекты, итераторы и генераторы в Python я уже затрагивал тему генераторов. В этой статье разберемся с тем, как работает оператор yield , и в чем разница между генераторами и корутинами. Будет проще понять эту статью, если прочитаете предыдущую.

Генераторы

Генератор – функция, которая генерирует последовательность значений, вместо одного значения, как это делает обычная функция. Любая функция, в которой есть оператор yield является генераторной:

>>> def fibonacci(): . a, b = 0, 1 . while True: . yield a . a, b = b, a + b >>> # Работаем с функцией вручную >>> f = fibonacci() >>> next(f) 0 >>> next(f) 1 >>> . >>> # Через цикл for >>> for num in fibonacci(): . if num > 42: . break # иначе цикл будет бесконечный . print(num) 0 1 1 2 3 .

Функция порождает (производит) числа Фибоначчи по одному через оператор yield . После каждого yield , генераторная функция приостанавливается и выполнение программы переходит к вызывающей стороне. Генераторная функция продолжает работу после вызова функции next(…) .

В примере с числами Фибоначчи, генераторная функция работает бесконечно. Она ничего не возвращает ( return ). Если генераторная функция завершает работу и в конце возвращает какое-то значение, то после этого выбрасывается исключение StopIteration . Это исключение можно словить и получить значение, которое вернул генератор:

>>> def gen(): . yield 'Yield something' . return 'Return something' >>> g = gen() >>> next(g) 'Yield something' >>> try: . next(g) . except StopIteration as exc: . print(exc.value) Return something

Напомню, что любая функция возвращает какое-то значение. Если оператор return не указан явно, то функция возвращает None . Поэтому, после завершения работы генераторной функции, исключение StopIteration выбрасывается в любом случае.

Корутины

Генераторная функция порождает значения для вызывающей стороны через оператор yield . Однако, генераторная функция также может и получать значения от вызывающей стороны. Генераторы, которые получают данные от вызывающей стороны называются корутинами (сопрограммами на русском). Все корутины являются генераторами. Но не наоборот, не путайте.

Передача данных в генератор осуществляется через тот же оператор yield . Только при получении данных, оператор yield находится в правой части выражения:

>>> def double(): . print('> Начало функции') . value = 2 * (yield) . print('> value = <>'.format(value)) . yield value . print('> Конец функции') >>> d = double() >>> next(d) > Начало функции >>> d.send(21) # метод send объекта генератора передает данные в функцию > value = 42 42 >>> d.send(42) > Конец функции Traceback (most recent call last): File "", line 1, in StopIteration

Последовательность работы функции double показана на картинке:

Как вы уже поняли, yield – двухсторонний оператор. Сначала генераторная функция передает значение вызывающей стороне ( yield something ). Затем останавливается и ждет, пока вызывающая сторона не передаст ей что-нибудь в ответ ( generator.send(something) ), чтобы она могла сохранить это значение ( something = yield ) и продолжить работу до следующего оператора yield .

yield является двухсторонним оператором всегда, даже если вы не передаете значение генератору через send , а просто пытаетесь продолжить работу генератора через next :

>>> def hello(): . value = yield 'Hello' . print('value = <>'.format(value)) >>> gen = hello() >>> next(gen) 'Hello' >>> next(gen) value = None # так как мы ничего не передали в генератор Traceback (most recent call last): File "", line 1, in StopIteration

Аналогично и в ситуации, когда yield используется только для получения данных. В таком случае, yield передает None вызывающей стороне:

>>> def simple_coroutine(): . value = yield . print(value) >>> coro = simple_coroutine() >>> from_coro = next(coro) >>> print(from_coro) None >>> coro.send(42) 42 Traceback (most recent call last): File "", line 1, in StopIteration

Заключение

В статье разобрались с оператором yield , и в чем разница между генераторами и корутинами.

Хоть генераторы и чем-то схожи с корутинами, но корутины довольно объемная тема, в которой много чего еще интересного. От оператора yield from и до пакета asyncio , который, по сути, работает на корутинах. В статье я затронул лишь самые основы.

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

Ваш адрес email не будет опубликован. Обязательные поля помечены *