[Перевод] Полное руководство по модулю asyncio в Python. Часть 2

Публикуем вторую часть руководства по модулю asyncio в Python, в которой представлены разделы оригинала №3 и 4. Читать головокружительную первую часть.

29913496cafa2306d21a18683b3ba883.png

3. Когда стоит использовать модуль asyncio

Если описать модуль asyncio несколькими словами, то получится, что это — нечто новое, популярное, широко обсуждаемое и интересное.

Но, несмотря на это, вопрос о том, когда в некий проект стоит внедрить asyncio, это — вопрос спорный и неоднозначный.

Когда же asyncio стоит использовать в Python-проектах?

3.1. Причины использования asyncio в Python-проектах

Вероятно, можно назвать три основные причины использования asyncio в Python-проектах:

  1. Чтобы применять в программе корутины.

  2. Чтобы воспользоваться парадигмой асинхронного программирования.

  3. Чтобы использовать неблокирующую обработку ввода/вывода.

Рассмотрим каждую из этих причин подробнее

Причина 1: корутины

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

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

Корутины — это один из элементов конкурентного программирования, наподобие потоков или процессов.

Конкурентность, основанная на потоках, доступна Python-программистам благодаря модулю threading, она поддерживается на уровне операционной системы, в которой работает Python-программа. Такая конкурентность подходит для обработки блокирующих операций ввода/вывода, таких, как запись и чтение данных при работе с файлами, сокетами, устройствами.

Конкурентность, основанная на процессах, реализуется на основе механизмов из модуля multiprocessing. Её, как и в случае с потоками, поддерживает операционная система. Она подходит для решения задач, скорость выполнения которых привязана к производительности CPU, но которые при этом не предусматривают интенсивного межпроцессного взаимодействия. Это, например, тяжёлые вычислительные задачи.

Корутины — это альтернатива потокам и процессам. Программисту их предоставляет сам язык Python и среда его выполнения (стандартный интерпретатор), а их поддержка основана на модуле asyncio. Они подходят для обработки неблокируюих операций ввода/вывода с использованием подпроцессов и сокетов. Впрочем, заниматься блокирующим вводом/выводом и решать тяжёлые вычислительные задачи можно, используя подход, имитирующий решение неблокирующих задач, когда в недрах программы всё сводится к применению потоков и процессов.

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

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

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

Корутины — это подпрограммы (функции), выполнение которых можно приостанавливать и возобновлять. Их выполнение приостанавливается с помощью выражения await, а возобновляется после того, как это выражение разрешится.

Корутины задуманы именно такими, это позволяет им кооперироваться, решая — когда и где приостановить выполнение.

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

Одного этого может быть достаточно для того чтобы принять решение о внедрении в проект модуля asyncio.

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

Они «легче» потоков. То есть — быстрее запускаются и используют меньше памяти. В сущности, корутины — это особая разновидность функций, а вот потоки представлены Python-объектами и связаны с потоками в операционной системе, с которыми эти объекты должны взаимодействовать.

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

Программист может решить применять корутины из-за их масштабируемости.

Причина 2: асинхронное программирование

Решение о том, что в некоем проекте нужен модуль asyncio, можно принять, руководствуясь необходимостью применения в нём методов асинхронного программирования.

То есть — когда нужно разработать Python-программу, в которой применяется парадигма асинхронного программирования.

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

В программировании «асинхронность» означает, что выполнение некоего действия запрашивается, но во время запроса это действие не выполняется. Оно выполняется позже.

Под асинхронным программированием часто понимают проектирование программы вокруг концепции асинхронных вызовов функций и задач.

Хотя существуют и другие способы реализации элементов асинхронного программирования в Python, полноценное асинхронное программирование в Python требует использования корутин и модуля asyncio.

Это — Python-библиотека, которая позволяет нам выполнять код, используя модель асинхронного программирования.

Python Concurrency with asyncio, 2022, с. 3

Решить использовать asyncio можно из-за того, что в программе нужно применять модуль асинхронного программирования, и это — оправданная причина.

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

Как мы уже видели, корутины могут выполнять неблокирующие операции ввода/вывода асинхронно. Но модуль asyncio, кроме того, даёт нам возможность выполнять в асинхронном стиле блокирующие операции ввода/вывода и задачи, скорость выполнения которых привязана к производительности CPU. Делается это путём имитации неблокирующих механизмов в недрах системы посредством потоков и процессов.

Причина 3: неблокирующий ввод/вывод

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

Ввод/вывод (Input/Output, I/O) — это чтение или запись данных при работе с неким ресурсом.

Среди распространённых примеров таких ресурсов можно отметить следующие:

  • Жёсткие диски: операции с файлами — чтение и запись данных, присоединение данных к файлу, переименование, удаление файлов и другие операции.

  • Периферийные устройства: мышь, клавиатура, дисплей, принтер, последовательный порт, камера и так далее.

  • Интернет: загрузка и выгрузка файлов, получение данных веб-страниц, выполнение запросов к RSS-ресурсам и так далее.

  • Базы данных: выборка, обновление, удаление информации посредством SQL запросов и выполнение других подобных операций.

  • Электронная почта: отправка и получение почты, обращение к почтовому ящику и другие операции.

Эти операции медленны в сравнении со скоростью, с которой выполняются вычисления в процессоре.

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

В результате эти операции часто называют блокирующими задачами ввода/вывода.

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

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

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

Неблокирующий ввод/вывод — это альтернатива блокирующему вводу/выводу.

Неблокирующий ввод/вывод требует поддержки этого механизма операционной системой, в которой работает программа, точно так же, как и блокирующий ввод/вывод. Все современные операционные системы поддерживают ту или иную форму неблокирующего ввода/вывода.

Неблокирующий ввод/вывод позволяет выполнять вызов операций чтения и записи данных в виде асинхронных запросов.

Операционная система обрабатывает такие запросы и уведомляет вызывающую программу тогда, когда будут доступны их результаты.

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

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

Комбинация неблокирующего ввода/вывода и асинхронного программирования так распространена, что её обычно сокращённо называют асинхронным вводом/выводом.

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

Модуль asyncio был добавлен в Python, в частности, для того, чтобы оснастить стандартную библиотеку языка поддержкой неблокирующего ввода/вывода с использованием подпроцессов (например — выполнение команд в операционной системе) и с использованием потоков (например — программирование TCP-сокетов).

Можно сымитировать неблокирующую обработку ввода/вывода с использованием потоков и возможностей асинхронного программирования, предоставляемых пулами потоков Python или исполнителями пулов потоков.

Модуль asyncio даёт нам стандартную поддержку асинхронного программирования для организации неблокирующего ввода/вывода посредством корутин, циклов событий и объектов, представляющих неблокирующие подпроцессы и потоки.

Решить использовать asyncio можно из-за желания применения асинхронного ввода/вывода в своих программах. И это — достойный повод для принятия такого решения.

3.2. Другие причины использования asyncio

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

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

Вот ещё несколько причин использования asyncio в некоем проекте:

  • Кто-то другой принял такое решение.

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

  • Разработчик, решив использовать asyncio, хочет лучше изучить этот модуль.

Программисты не всегда обладают полным контролем над проектами, над которыми они работают.

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

Одной из таких технологий может быть asyncio.

Использование asyncio при работе над существующим проектом может быть объяснено тем, что этот модуль в нём уже используется. То есть, речь уже не идёт о выборе — программист вынужден использовать то, что уже применяется в проекте.

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

Например:

  • Вам нужно использовать сторонний API, а в примерах кода к этому API применяется asyncio.

  • Вам нужно интегрировать в свой проект существующее опенсорсное решение, в котором используется asyncio.

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

Если у вас нет альтернатив тому решению, где используется asyncio, применение этого модуля может быть попросту навязано вам в тот момент, когда вы выбираете это решение.

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

Кому-то эта причина может показаться несерьёзной: «А как же требования к проекту?».

Дело в том, что если некто решит внедрить в проект asyncio просто потому, что ему хочется опробовать этот модуль, то это — вполне достойная причина так поступить.

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

3.3. Когда не стоит использовать asyncio

Мы потратили немало времени, говоря о том, когда стоит использовать asyncio.

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

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

Такой подход, правда, может приводить к принятию ошибочных решений. Дело в том, что asyncio может понадобиться в проекте и по причинам, о которых мы не говорили.

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

Полагаю, что главная причина неиспользования asyncio в проекте заключается в том, что этот модуль не даёт тех плюсов, которые, как считает разработчик, он должен давать проекту.

Вокруг конкурентного выполнения код в Python, а особенно — вокруг asyncio, существует множество заблуждений.

Например — вот что можно слышать о модуле asyncio:

  • Он способен решить проблему глобальной блокировки интерпретатора (Global Interpreter Lock, GIL).

  • Он быстрее, чем потоки.

  • Он избавляет разработчика от необходимости использовать мьютексы и другие примитивы синхронизации.

  • Им легче пользоваться, чем потоками.

Всё это — неправда.

В некий момент времени может выполняться лишь код одной корутины — так уж они устроены. Они, при выполнении, кооперируются. Это очень похоже на то, как работают потоки под GIL. На самом деле, GIL к нашему делу не относится. Вероятно, в большинстве ситуаций, когда используется asyncio, разговоры о GIL не играют особой роли.

Любую программу, которую можно написать, пользуясь asyncio, можно написать и с помощью потоков. Она, вероятно, будет столь же быстрой, или быстрее, чем её asyncio-версия. Ещё она, скорее всего, окажется проще, а другим разработчикам будет проще читать и понимать её код.

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

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

Асинхронное программирование пользуется популярностью уже немалое время, к нему хорошо относятся программисты, пишущие на множестве языков, прежде всего — на JavaScript.

Асинхронное программирование отличается от процедурного, объектно-ориентированного, функционального программирования. Некоторым программистам оно просто не нравится.

Если в вашем случае так оно и есть — не проблема. Не нравится — не пользуйтесь. Это — обоснованная причина отказа от asyncio.

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

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

4. Корутины в Python

В Python имеется встроенная поддержка типа coroutine и новых выражений — наподобие async def и await.

Модуль asyncio в Python позволяет запускать корутины и разрабатывать асинхронные программы.

В этом разделе мы гораздо ближе, чем прежде, познакомимся с корутинами.

4.1. Что такое корутина

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

Корутины часто называют более общими формами подпрограмм.

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

В частности, корутины могут управлять тем, где именно они приостанавливают своё выполнение.

Для этого может привлекаться специфическое выражение — такое, как await, похожее на выражение yield, применяемое в Python-генераторах.

Корутина — это метод, который можно приостановить, когда у нас имеется задача, выполнение которой может занять много времени. Потом, когда эта задача будет завершена, метод можно возобновить. В Python 3.5. была реализована встроенная поддержка корутин и асинхронного программирования, тогда в язык были добавлены ключевые слова async и await.

Python Concurrency with asyncio, 2022, с. 3

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

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

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

Effective Python, 2019, с. 267

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

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

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

Cooperative multitasking, Wikipedia

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

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

4.2. Сравнение корутин с программами и подпрограммами

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

Но, возможно, правильнее будет говорить о том, что «программа» — это самостоятельная программа, а «подпрограмма» — это функция в «программе».

Получается, что у программ есть подпрограммы.

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

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

Подпрограмма запускается, выполняет содержащиеся в ней выражения и каким-то образом завершает работу, возвращая управление в место своего вызова. Обычно подпрограммы вызываются другими подпрограммами.

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

Корутины во многом похожи на подпрограммы. Например:

  • И те и другие — это отдельные именованные модули, содержащие выражения.

  • И те и другие могут принимать аргументы, а могут и не принимать.

  • И те и другие могут возвращать значения или их не возвращать.

Главное их различие заключается в том, что корутины могут самостоятельно приостанавливать и возобновлять своё выполнение много раз до возврата значения и выхода из них.

И корутины, и подпрограммы могут вызывать другие сущности, подобные себе. Подпрограмма может вызывать другие подпрограммы. Корутина способна вызывать другие корутины. Но корутина может ещё и вызывать другие подпрограммы.

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

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

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

4.3. Сравнение корутин и генераторов

Генератор — это особая функция, которая может приостанавливать своё выполнение.

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

Python glossary

Функцию-генератор можно определить как обычную функцию, но она использует выражение yield в том месте, где приостанавливает выполнение и возвращает значение.

Функция-генератор возвращает объект итерируемого генератора, который можно обходить — например — с помощью цикла for. Каждый раз, когда запускается генератор, он выполняется с последнего места, где был приостановлен, до следующего выражения yield.

Итерируемый генератор: объект, созданный функцией-генератором. Каждое выражение yield временно приостанавливает его работу, при этом запоминается состояние выполнения в том месте, где была приостановлена работа (включая локальные переменные и ожидающие разрешения выражения try). Когда итерируемый генератор возобновляет работу — он начинает с того места, где остановился (в отличие от функций, которые, при каждом вызове, начинают работать заново).

Python glossary

Корутина может приостанавливать работу или передавать управление другой корутине, используя выражение await. После завершения работы вызванной корутины вызвавшая её корутина продолжит работу с того места, где была приостановлена.

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

High Performance Python, 2020, с. 218

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

Генераторы, ещё известные как полукорутины, это — подмножество корутин.

Coroutine, Wikipedia

До того, как были созданы корутины, возможности генераторов были расширены до такого уровня, что они могли использоваться в Python-программах подобно корутинам.

Это требовало серьёзных технических знаний о генераторах и о разработке собственных планировщиков задач.

Для реализации собственной конкурентной системы с использованием генераторов нужно, в первую очередь,   обладать фундаментальным пониманием функций-генераторов и того, как работает выражение yield. В частности, базовый принцип поведения команды yield заключается в том, что её использование приводит к приостановке выполнения кода генератора. Приостановка выполнения кода позволяет написать планировщик, который рассматривает генераторы как нечто вроде «задач» и чередует их выполнение, используя некое подобие переключения задач.

Python Cookbook, 2013, с. 524

Это стало возможным благодаря изменениям, внесённым в генераторы, и введению в Python выражения yield from.

Позже эти нововведения были признаны устаревшими ради содействия более современным выражениям async/await.

4.4. Сравнение корутин и задач

Подпрограммы и корутины могут представлять в программах «задачи».

Но в Python есть особый объект, представляющий задачу. Это — объект asyncio.Task.

Объект, похожий на объект Future, который отвечает за выполнение Python-корутин. […] Задачи используются для выполнения корутин в цикле событий.

Asyncio Task Object

Корутину можно обернуть в объект asyncio.Task и выполнить независимо — в противовес её выполнению напрямую, внутри другой корутины. Объект Task даёт средства для асинхронного выполнения корутин.

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

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

Объект Task не может существовать сам по себе. В него обязательно должна быть обёрнута корутина.

В результате получается, что объект Task, представляющий задачу — это корутина, но корутина — это не задача.

4.5. Сравнение корутин и потоков

Корутины «легче» потоков.

Корутина объявляется в виде функции.

Поток — это объект, который создаёт и которым управляет операционная система. Он представлен в Python объектом threading.Thread.

Поток: им управляет операционная система, он представлен Python-объектом.

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

Цена запуска корутины — это цена вызова функции. После того, как корутина активируется, она использует меньше 1 Кб памяти до тех пор, пока не отработает.

Effective Python, 2019, с. 267

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

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

Effective Python, 2019, с. 267

Подробнее о потоках можно почитать здесь.

4.6. Сравнение корутин и процессов

Корутины «легче» процессов. На самом же деле — даже потоки «легче» процессов».

Процесс — это компьютерная программа. У неё может быть один или большее количество потоков.

Python-процесс — это отдельный экземпляр Python-интерпретатора.

Процессы, как и потоки, создаёт операционная система, она же ими управляет. Они представлены объектом multiprocessing.Process.

Процесс: им управляет операционная система, он представлен Python-объектом.

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

Корутина — это всего лишь особая функция, а процесс — это экземпляр интерпретатора, в котором имеется по меньшей мере один поток.

Если вас интересуют подробности о Python-процессах — загляните сюда.

4.7. Когда корутины были добавлены в Python

Корутины в Python расширили возможности генераторов.

Генераторы медленно и долго двигались к тому, чтобы превратиться в стандартные Python-корутины.

Мы можем исследовать некоторые основные изменения, внесённые в Python ради добавления в язык корутин. Эти изменения мы можем рассматривать как предвестники добавления в язык asyncio.

В генераторы были добавлены новые методы, вроде send() и close(), что позволило приблизить их поведение к поведению корутин.

Они появились в Python 2.5 и описаны в PEP 342.

Это PEP предлагает некоторые расширения API и синтаксиса генераторов, чтобы их можно было использовать как простые корутины.

PEP 342 — Coroutines via Enhanced Generators

Позже генераторам позволили выдавать исключение SuspendIteration, а так же — исключение StopIteration, описанные в PEP 334.

Это PEP предлагает ограниченный подход к созданию корутин, основанный на исключениях протокола итератора. В настоящее время итератор может возбудить исключение StopIteration для указания на то, что он завершил выдавать значения. Это предложение добавляет к данному протоколу ещё одно исключение, SuspendIteration, которое указывает на то, что у некоего итератора могут быть ещё значения, но он в настоящий момент не способен выдать одно из таких значений.

PEP 334 — Simple Coroutines via SuspendIteration

Подавляющее большинство возможностей работы с современными корутинами посредством модуля asyncio было описано в PEP 3156, добавленное в Python 3.3.

Это — предложение по реализации асинхронного ввода/вывода в Python 3, начиная с Python 3.3. Считайте это конкретным предложением возможностей, которых не хватает в PEP 3153. Это предложение включает в себя описание подключаемого цикла событий, абстракций транспортного уровня и уровня протоколов, похожих на те, что имеются в Twisted, описание высокоуровневого планировщика, основанного на yield from (PEP 380). Предлагаемое имя пакета — asyncio.

PEP 3156 — Asynchronous IO Support Rebooted: the «asyncio» Module

Второй подход к работе с корутинами, основанный на генераторах, был добавлен в Python 3.4 в виде расширения Python-генераторов.

Корутины были определены как функции, использующие декоратор @asyncio.coroutine.

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

Корутина могла приостанавливаться и выполнять другую корутину, используя выражение yield from.

Например:

# объявление собственной корутины в Python 3.4
@asyncio.coroutine
def custom_coro():
    # приостановка и выполнение другой корутины
    yield from asyncio.sleep(1)

Выражение yield from было определено в PEP 380.

Предлагаемая синтаксическая конструкция предназначена для генераторов и позволяет делегировать часть их функционала другим генераторам. Благодаря этому можно выделить блок кода, содержащий yield, и поместить его в другой генератор.

PEP 380 — Syntax for Delegating to a Subgenerator

Выражением yield from всё ещё можно пользоваться в генераторах, хотя этот подход признан устаревшим для приостановки выполнения корутин. Вместо него рекомендуется использовать выражение await.

Обратите внимание: поддержка корутин, основанных на генераторах, признана устаревшей и удаляется из Python 3.11. Корутины, основанные на генераторах, появились до синтаксической конструкции async/await. Это — Python-генераторы, которые используют выражения yield from для ожидания на объектах Future и на других корутинах.

Asyncio Coroutines and Tasks

Мы можем сказать, что корутины были добавлены в Python в виде стандартных возможностей языка в версии 3.5.

Сюда входят изменения, внесённые в язык, такие, как выражения async def, await, async with и async for, а так же — тип coroutine.

Эти изменения были описаны в PEP 492.

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

PEP 492 — Coroutines with async and await synt

© Habrahabr.ru