Этот вопрос мотивирован моим другим вопросом: как ждать в cdef?
В сети есть масса статей и сообщений в блогах asyncio
, но все они очень поверхностны. Я не смог найти никакой информации о том, как asyncio
это на самом деле реализовано, и что делает ввод-вывод асинхронным. Я пытался прочитать исходный код, но это тысячи строк кода C не самого высокого уровня, многие из которых имеют дело с вспомогательными объектами, но, что наиболее важно, трудно связать синтаксис Python и то, какой код C он будет переводить в.
Собственная документация Asycnio еще менее полезна. Там нет информации о том, как это работает, только некоторые инструкции о том, как его использовать, которые также иногда вводят в заблуждение / очень плохо написаны.
Я знаком с реализацией сопрограмм в Go и как бы надеялся, что Python сделает то же самое. Если бы это было так, код, который я написал в сообщении, указанном выше, работал бы. Поскольку этого не произошло, теперь я пытаюсь понять, почему. Мое лучшее предположение таково, пожалуйста, поправьте меня, где я ошибаюсь:
- Определения процедур формы
async def foo(): ...
фактически интерпретируются как методы наследника классаcoroutine
. - Возможно,
async def
фактически разделен на несколько методовawait
операторами, где объект, для которого эти методы вызываются, может отслеживать прогресс, достигнутый им в выполнении на данный момент. - Если вышесказанное верно, то, по сути, выполнение сопрограммы сводится к вызову методов объекта сопрограммы каким-либо глобальным менеджером (циклом?).
- Глобальный менеджер каким-то образом (как?) Знает, когда операции ввода-вывода выполняются кодом Python (только?), И может выбрать один из ожидающих методов сопрограммы для выполнения после того, как текущий выполняющийся метод отказался от управления (нажмите на
await
оператор ).
Другими словами, вот моя попытка «десугарировать» некоторый asyncio
синтаксис во что-то более понятное:
async def coro(name):
print('before', name)
await asyncio.sleep()
print('after', name)
asyncio.gather(coro('first'), coro('second'))
# translated from async def coro(name)
class Coro(coroutine):
def before(self, name):
print('before', name)
def after(self, name):
print('after', name)
def __init__(self, name):
self.name = name
self.parts = self.before, self.after
self.pos = 0
def __call__():
self.parts[self.pos](self.name)
self.pos += 1
def done(self):
return self.pos == len(self.parts)
# translated from asyncio.gather()
class AsyncIOManager:
def gather(*coros):
while not every(c.done() for c in coros):
coro = random.choice(coros)
coro()
Если моя догадка окажется верной: тогда у меня проблема. Как на самом деле происходит ввод-вывод в этом сценарии? В отдельной ветке? Приостановлен ли весь интерпретатор, а ввод-вывод происходит вне интерпретатора? Что именно подразумевается под вводом-выводом? Если моя процедура python вызвала процедуру C open()
, а она, в свою очередь, отправила прерывание ядру, передав ему управление, как интерпретатор Python знает об этом и может продолжить выполнение некоторого другого кода, в то время как код ядра выполняет фактический ввод-вывод и до тех пор, пока он пробуждает процедуру Python, которая изначально отправила прерывание? Как интерпретатор Python в принципе может знать об этом?
BaseEventLoop
реализован CPython : github.com/python/cpython/blob/…_run_once
, которая на самом деле является единственной полезной функцией во всем этом модуле, сделана «частной»? Реализация ужасная, но это меньшая проблема. Почему единственная функция, которую вы когда-либо хотели бы вызвать в цикле событий, помечена как «не звоните мне»?_run_once
в первую очередь?_run_once
?asyncio
является сложным и имеет свои недостатки, но, пожалуйста, продолжайте обсуждение вежливо. Не ругайте разработчиков, стоящих за кодом, которого вы сами не понимаете.Ответы:
Как работает asyncio?
Прежде чем ответить на этот вопрос, нам нужно понять несколько основных терминов, пропустите их, если вы уже знаете какие-либо из них.
Генераторы
Генераторы - это объекты, которые позволяют нам приостановить выполнение функции Python. Созданные пользователем генераторы реализуются с использованием ключевого слова
yield
. Создавая обычную функцию, содержащуюyield
ключевое слово, мы превращаем эту функцию в генератор:Как видите, вызов
next()
генератора заставляет интерпретатор загружать фрейм теста и возвращатьyield
значение ed. Приnext()
повторном вызове заставляет фрейм снова загружаться в стек интерпретатора и продолжатьyield
ввод другого значения.К третьему
next()
вызову наш генератор закончился, иStopIteration
его бросили.Общение с генератором
Менее известной особенностью генераторов является тот факт, что вы можете общаться с ними двумя способами:
send()
иthrow()
.После вызова
gen.send()
значение передается как возвращаемое значение изyield
ключевого слова.gen.throw()
с другой стороны, позволяет генерировать исключения внутри генераторов, за исключением случаев, когда ониyield
вызываются в том же месте .Возврат значений от генераторов
Возврат значения от генератора приводит к тому, что значение помещается в
StopIteration
исключение. Позже мы можем восстановить значение из исключения и использовать его для наших нужд.Вот новое ключевое слово:
yield from
Python 3.4 пришел с добавлением нового ключевого слова:
yield from
. То , что это ключевое слово позволяет нам сделать, это пройти по любомуnext()
,send()
иthrow()
в внутреннепризматический наиболее вложенный генератор. Если внутренний генератор возвращает значение, оно также является возвращаемым значениемyield from
:Я написал статью, чтобы подробнее остановиться на этой теме.
Собираем все вместе
После введения нового ключевого слова
yield from
в Python 3.4 мы теперь могли создавать генераторы внутри генераторов, которые, как туннель, передают данные туда и обратно от самого внутреннего к самому внешнему генератору. Это породило новое значение для генераторов - сопрограмм .Сопрограммы - это функции, которые можно останавливать и возобновлять во время работы. В Python они определяются с помощью
async def
ключевого слова. Так же, как генераторы, они также используют свою собственную форму ,yield from
котораяawait
. Доasync
иawait
были введены в Python 3.5, мы создали сопрограммы в точно так же, генераторах были созданы (сyield from
вместоawait
).Как и любой итератор или генератор, реализующий
__iter__()
метод, сопрограммы реализуют,__await__()
что позволяет им продолжать работу каждый раз приawait coro
вызове.Внутри документации Python есть хорошая диаграмма последовательности, которую вы должны проверить.
В asyncio, помимо функций сопрограмм, у нас есть 2 важных объекта: задачи и фьючерсы .
Фьючерсы
Фьючерсы - это объекты, в которых
__await__()
реализован метод, и их задача - сохранять определенное состояние и результат. Состояние может быть одним из следующих:fut.cancel()
fut.set_result()
исключений с использованиемfut.set_exception()
Результатом, как вы уже догадались, может быть либо объект Python, который будет возвращен, либо исключение, которое может возникнуть.
Другой важной особенностью
future
объектов является то, что они содержат метод с именемadd_done_callback()
. Этот метод позволяет вызывать функции, как только задача будет выполнена - независимо от того, вызвало ли она исключение или завершилось.Задачи
Объекты задач - это специальные фьючерсы, которые охватывают сопрограммы и взаимодействуют с самыми внутренними и самыми внешними сопрограммами. Каждый раз, когда сопрограмма
await
передает future, будущее полностью передается задаче (как в примереyield from
), и задача получает его.Далее задача привязывается к будущему. Он делает это, обращаясь
add_done_callback()
к будущему. С этого момента, если будущее когда-либо будет выполнено, либо путем отмены, либо передачи исключения, либо в результате передачи объекта Python, будет вызван обратный вызов задачи, и он вернется к существованию.Asyncio
Последний животрепещущий вопрос, на который мы должны ответить, - как реализован ввод-вывод?
Глубоко внутри asyncio у нас есть цикл обработки событий. Цикл событий задач. Задача цикла обработки событий - вызывать задачи каждый раз, когда они готовы, и координировать все эти усилия на одной рабочей машине.
Часть IO цикла событий построена на единственной вызываемой критически важной функции
select
. Select - это функция блокировки, реализуемая операционной системой ниже и позволяющая ожидать на сокетах входящих или исходящих данных. После получения данных он просыпается и возвращает сокеты, которые получили данные, или сокеты, готовые к записи.Когда вы пытаетесь получить или отправить данные через сокет через asyncio, на самом деле ниже происходит то, что сокет сначала проверяется, есть ли в нем какие-либо данные, которые можно немедленно прочитать или отправить. Если его
.send()
буфер заполнен или.recv()
буфер пуст, сокет регистрируется вselect
функции (путем простого добавления его в один из списковrlist
дляrecv
иwlist
дляsend
), а соответствующая функция представляет собойawait
вновь созданныйfuture
объект, связанный с этим сокетом.Когда все доступные задачи ожидают фьючерсов, цикл событий вызывает
select
и ждет. Когда в один из сокетов поступают входящие данные или егоsend
буфер исчерпан, asyncio проверяет будущий объект, привязанный к этому сокету, и устанавливает его в состояние «Готово».Теперь все волшебство происходит. Будущее настроено на выполненное, задача, которая добавлялась раньше с помощью
add_done_callback()
, возвращается к жизни и вызывает.send()
сопрограмму, которая возобновляет внутреннюю сопрограмму (из-заawait
цепочки), и вы читаете недавно полученные данные из ближайшего буфера, который он пролился на.Снова цепочка методов в случае
recv()
:select.select
ждет.future.set_result()
называется.add_done_callback()
теперь просыпается..send()
сопрограмму, которая полностью входит в самую внутреннюю сопрограмму и пробуждает ее.Таким образом, asyncio использует возможности генератора, которые позволяют приостанавливать и возобновлять функции. Он использует
yield from
возможности, которые позволяют передавать данные туда и обратно от самого внутреннего генератора к самому внешнему. Он использует все это, чтобы остановить выполнение функции, пока он ожидает завершения ввода-вывода (с помощьюselect
функции ОС ).А лучше всего? Пока одна функция приостановлена, другая может работать и чередоваться с деликатной тканью, что является асинхронным.
источник
yield from
работают генераторы . Однако я заметил вверху, что его можно пропустить, если читатель уже знает об этом :-) Что еще, по вашему мнению, я должен добавить?select
может быть квалифицировано, поскольку именно так работают в ОС системные вызовы неблокирующего ввода-вывода. Фактическиеasyncio
конструкции и цикл событий - это просто код уровня приложения, созданный из этих вещей.Речь
async/await
иasyncio
не об одном и том же. Первая - это фундаментальная низкоуровневая конструкция (сопрограммы), а вторая - это библиотека, использующая эти конструкции. И наоборот, нет однозначного ответа.Ниже приводится общее описание того, как работают
async/await
и-asyncio
подобные библиотеки. То есть, сверху могут быть другие уловки (есть ...), но они несущественны, если вы сами их не построите. Разница должна быть незначительной, если вы уже не знаете достаточно, чтобы не задавать такой вопрос.1. Сопрограммы против подпрограмм в скорлупе
Точно так же, как подпрограммы (функции, процедуры, ...), сопрограммы (генераторы, ...) представляют собой абстракцию стека вызовов и указателя инструкций: существует стек выполняемых частей кода, и каждая из них находится в определенной инструкции.
Различие между
def
противопоставлениемasync def
используется просто для ясности. Фактическая разница -return
противyield
. От этогоawait
илиyield from
взять разницу от отдельных вызовов до целых стеков.1.1. Подпрограммы
Подпрограмма представляет новый уровень стека для хранения локальных переменных и однократный обход ее инструкций для достижения конца. Рассмотрим такую подпрограмму:
Когда вы его запускаете, это означает
bar
иqux
return
передайте его значение в стек вызововПримечательно, что 4. означает, что подпрограмма всегда запускается в одном и том же состоянии. После завершения все, что относится только к самой функции, теряется. Функцию нельзя возобновить, даже если после нее есть инструкции
return
.1.2. Сопрограммы как постоянные подпрограммы
Сопрограмма похожа на подпрограмму, но может завершиться без разрушения своего состояния. Рассмотрим такую сопрограмму:
Когда вы его запускаете, это означает
bar
иqux
yield
, поместите его значение в вызывающий стек, но сохраните указатель стека и инструкцииyield
, восстановить стек и указатель инструкции и передать аргументы вqux
return
передайте его значение в стек вызововОбратите внимание на добавление 2.1 и 2.2 - сопрограмма может быть приостановлена и возобновлена в заранее определенных точках. Это похоже на то, как подпрограмма приостанавливается во время вызова другой подпрограммы. Разница в том, что активная сопрограмма не привязана строго к своему стеку вызовов. Вместо этого приостановленная сопрограмма является частью отдельного изолированного стека.
Это означает, что подвешенные сопрограммы можно свободно хранить или перемещать между стеками. Любой стек вызовов, имеющий доступ к сопрограмме, может решить возобновить ее.
1.3. Обход стека вызовов
Пока наша сопрограмма идет вниз по стеку вызовов только с
yield
. Подпрограмма может перемещаться вниз и вверх по стеку вызовов с помощьюreturn
и()
. Для полноты сопрограмм также нужен механизм для подъема по стеку вызовов. Рассмотрим такую сопрограмму:Когда вы его запускаете, это означает, что он по-прежнему выделяет стек и указатель инструкций как подпрограмму. Когда он приостанавливается, это по-прежнему похоже на сохранение подпрограммы.
Однако
yield from
делает и то , и другое . Он приостанавливаетwrap
и запускает указатель стека и инструкцииcofoo
. Обратите внимание, чтоwrap
остается приостановленным доcofoo
полного завершения. Всякий раз, когдаcofoo
приостанавливается или что-то отправляется,cofoo
он напрямую подключается к вызывающему стеку.1.4. Все сопрограммы вниз
Как установлено,
yield from
позволяет соединить два прицела через другой промежуточный. При рекурсивном применении это означает, что вершина стека может быть соединена с нижней частью стека.Учтите, что
root
иcoro_b
друг о друге не знают. Это делает сопрограммы намного чище, чем обратные вызовы: сопрограммы по-прежнему построены на соотношении 1: 1, как подпрограммы. Сопрограммы приостанавливают и возобновляют весь свой существующий стек выполнения до обычной точки вызова.Примечательно, что
root
можно было возобновить произвольное количество сопрограмм. Тем не менее, он никогда не может возобновиться более чем по одному одновременно. Сопрограммы одного и того же корня параллельны, но не параллельны!1.5. Python
async
иawait
Объяснение до сих пор явно используется
yield
иyield from
словарный запас генераторов - лежащий в основе функциональность та же. Новый синтаксис Python3.5async
иawait
существует в основном для ясности.async for
Иasync with
утверждения необходимы , потому что вы бы разорватьyield from/await
цепь с голымfor
иwith
отчетностью.2. Анатомия простого цикла событий
Сама по себе сопрограмма не имеет понятия о передаче управления другой сопрограмме. Он может передать управление только вызывающей стороне в нижней части стека сопрограмм. Затем этот вызывающий может переключиться на другую сопрограмму и запустить ее.
Этот корневой узел нескольких сопрограмм обычно представляет собой цикл событий : при приостановке сопрограмма выдает событие, на котором она хочет возобновить. В свою очередь, цикл событий может эффективно ожидать возникновения этих событий. Это позволяет ему решать, какую сопрограмму запускать следующей или как подождать перед возобновлением.
Такой дизайн подразумевает наличие набора заранее определенных событий, которые понимает цикл. Несколько сопрограмм
await
друг друга, пока, наконец, не будетawait
обработано событие . Это событие может напрямую связываться с циклом событий посредствомyield
управления.Ключ в том, что приостановка сопрограмм позволяет циклу событий и событиям напрямую взаимодействовать. Промежуточный стек сопрограмм не требует каких-либо знаний о том, в каком цикле он выполняется, или о том, как работают события.
2.1.1. События во времени
Самое простое событие для обработки - достижение определенного момента времени. Это также фундаментальный блок многопоточного кода: поток повторяется
sleep
до тех пор, пока условие не станет истинным. Однако обычноеsleep
выполнение блоков само по себе - мы хотим, чтобы другие сопрограммы не блокировались. Вместо этого мы хотим сообщить циклу событий, когда он должен возобновить текущий стек сопрограмм.2.1.2. Определение события
Событие - это просто значение, которое мы можем идентифицировать - будь то через перечисление, тип или другой идентификатор. Мы можем определить это с помощью простого класса, который хранит наше целевое время. Помимо хранения информации о событии, мы можем
await
напрямую разрешить класс.Этот класс только сохраняет событие - он не говорит, как на самом деле его обработать.
Единственная особенность
__await__
- это то, чтоawait
ищет ключевое слово. Фактически, это итератор, но он недоступен для обычного итерационного механизма.2.2.1. В ожидании события
Теперь, когда у нас есть событие, как на него реагируют сопрограммы? Мы должны иметь возможность выразить эквивалент
sleep
посредствомawait
нашего события. Чтобы лучше увидеть, что происходит, мы ждем дважды половину времени:Мы можем напрямую создать и запустить эту сопрограмму. Как и в случае с генератором, при использовании
coroutine.send
сопрограмма запускается до тех пор, пока не будетyield
получен результат.Это дает нам два
AsyncSleep
события, а затем,StopIteration
когда сопрограмма завершена. Обратите внимание, что единственная задержка - это отtime.sleep
цикла! КаждыйAsyncSleep
хранит только смещение от текущего времени.2.2.2. Событие + Сон
На данный момент в нашем распоряжении есть два отдельных механизма:
AsyncSleep
События, которые могут быть получены из сопрограммыtime.sleep
что может ждать, не влияя на сопрограммыПримечательно, что эти двое ортогональны: ни один не влияет на другой и не запускает его. В результате мы можем придумать нашу собственную стратегию,
sleep
чтобы противостоять задержке файлаAsyncSleep
.2.3. Наивный цикл событий
Если у нас несколько сопрограмм, каждая из них может сказать нам, когда она хочет, чтобы ее разбудили. Затем мы можем дождаться возобновления первого из них, затем следующего и так далее. Примечательно, что на каждом этапе нам важно только то, что будет следующим .
Это упрощает планирование:
Тривиальная реализация не требует каких-либо сложных концепций. A
list
позволяет сортировать сопрограммы по дате. Ожидание обычноеtime.sleep
. Запуск сопрограмм работает так же, как и раньше, сcoroutine.send
.Конечно, здесь есть много возможностей для улучшения. Мы можем использовать кучу для очереди ожидания или таблицу отправки для событий. Мы также можем получить возвращаемые значения из
StopIteration
и назначить их сопрограмме. Однако основной принцип остается прежним.2.4. Кооперативное ожидание
AsyncSleep
Событие иrun
цикл обработки событий является полностью работоспособно осуществлением своевременных мероприятий.При этом происходит совместное переключение между каждой из пяти сопрограмм, каждая из которых приостанавливается на 0,1 секунды. Несмотря на то, что цикл обработки событий является синхронным, он по-прежнему выполняет работу за 0,5 секунды вместо 2,5 секунд. Каждая сопрограмма хранит состояние и действует независимо.
3. Цикл событий ввода-вывода
Поддерживаемый цикл событий
sleep
подходит для опроса . Однако ожидание ввода-вывода для дескриптора файла может быть выполнено более эффективно: операционная система реализует ввод-вывод и, таким образом, знает, какие дескрипторы готовы. В идеале цикл событий должен поддерживать явное событие «готовность к вводу-выводу».3.1.
select
вызовУ Python уже есть интерфейс для запроса в ОС дескрипторов ввода-вывода для чтения. Когда вызывается с дескрипторами для чтения или записи, он возвращает дескрипторы, готовые к чтению или записи:
Например, мы можем
open
файл для записи и ждать, пока он будет готов:После возврата select
writeable
содержит наш открытый файл.3.2. Базовое событие ввода / вывода
Подобно
AsyncSleep
запросу, нам нужно определить событие для ввода-вывода. Согласно базовойselect
логике событие должно относиться к читаемому объекту, например кopen
файлу. Кроме того, мы храним, сколько данных нужно прочитать.Как и в случае с,
AsyncSleep
мы в основном просто сохраняем данные, необходимые для основного системного вызова. На этот раз__await__
его можно возобновлять несколько раз - пока желаемоеamount
не будет прочитано. Кроме того, мы получаемreturn
результат ввода-вывода, а не просто возобновляем.3.3. Дополнение цикла событий чтением ввода-вывода
Основа для нашего цикла событий по-прежнему
run
определена ранее. Во-первых, нам нужно отслеживать запросы на чтение. Это больше не отсортированное расписание, мы сопоставляем только запросы чтения с сопрограммами.Поскольку
select.select
принимает параметр тайм-аута, мы можем использовать его вместоtime.sleep
.Это дает нам все читаемые файлы - если они есть, мы запускаем соответствующую сопрограмму. Если таковых нет, значит, мы достаточно долго ждали запуска нашей текущей сопрограммы.
Наконец, мы должны фактически прослушивать запросы на чтение.
3.4. Собираем все вместе
Вышесказанное было небольшим упрощением. Нам нужно кое-что переключить, чтобы не истощать спящие сопрограммы, если мы всегда можем читать. Нам нужно справиться с тем, что нечего читать или нечего ждать. Однако конечный результат по-прежнему соответствует 30 LOC.
3.5. Кооперативный ввод / вывод
Реализации
AsyncSleep
,AsyncRead
иrun
теперь полностью функциональны для сна и / или чтения. Как и в случаеsleepy
, мы можем определить помощника для проверки чтения:Запустив это, мы видим, что наш ввод-вывод чередуется с ожидающей задачей:
4. Неблокирующий ввод-вывод
Хотя ввод-вывод в файлах передает эту концепцию, он не совсем подходит для библиотеки, например
asyncio
:select
вызов всегда возвращается для файлов , и обаopen
иread
могут блокироваться на неопределенное время . Это блокирует все сопрограммы цикла обработки событий - что плохо. Такие библиотеки, какaiofiles
использование потоков и синхронизации для имитации неблокирующего ввода-вывода и событий в файле.Однако сокеты допускают неблокирующий ввод-вывод, а присущая им задержка делает его гораздо более важным. При использовании в цикле событий ожидание данных и повторная попытка могут быть заключены в оболочку, ничего не блокируя.
4.1. Неблокирующее событие ввода / вывода
Подобно нашему
AsyncRead
, мы можем определить событие приостановки и чтения для сокетов. Вместо файла мы берем сокет, который должен быть неблокирующим. Кроме того, мы__await__
используемsocket.recv
вместоfile.read
.В отличие от
AsyncRead
,__await__
выполняет действительно неблокирующий ввод-вывод. Когда данные доступны, они всегда читаются. Когда данные недоступны, он всегда приостанавливается. Это означает, что цикл обработки событий блокируется только на время выполнения полезной работы.4.2. Разблокирование цикла событий
Что касается цикла событий, то здесь особо ничего не меняется. Событие для прослушивания остается таким же, как и для файлов - файловый дескриптор, отмеченный как готовый
select
.На этом этапе должно быть очевидно, что
AsyncRead
иAsyncRecv
являются событиями того же типа. Мы могли бы легко реорганизовать их в одно событие с заменяемым компонентом ввода-вывода. По сути, цикл событий, сопрограммы и события четко разделяют планировщик, произвольный промежуточный код и фактический ввод-вывод.4.3. Уродливая сторона неблокирующего ввода-вывода
В принципе, что вы должны сделать в этой точке повторить логику
read
какrecv
дляAsyncRecv
. Однако сейчас это намного уродливее - вам нужно обрабатывать ранние возвраты, когда функции блокируются внутри ядра, но передают вам управление. Например, открытие соединения по сравнению с открытием файла намного дольше:Короче говоря, осталось несколько десятков строк обработки исключений. На этом этапе события и цикл событий уже работают.
Дополнение
Пример кода на github
источник
yield self
в AsyncSleep дает мнеTask got back yield
ошибку, почему? Я вижу, что код asyncio.Futures использует это. Использование чистого урожая отлично работает.Ваше
coro
удаление сахара концептуально правильно, но немного неполно.await
не приостанавливает безоговорочно, но только если встречает блокирующий вызов. Как узнать, что звонок заблокирован? Это определяется ожидаемым кодом. Например, ожидаемая реализация чтения сокета может быть уменьшена до:В реальном asyncio эквивалентный код изменяет состояние a
Future
вместо того, чтобы возвращать магические значения, но концепция остается той же. При соответствующей адаптации к объекту, подобному генератору, приведенный выше код можноawait
редактировать.На стороне вызывающего, когда ваша сопрограмма содержит:
Он превращает сахар во что-то близкое к:
Люди, знакомые с генераторами, склонны описывать вышесказанное в терминах
yield from
автоматической приостановки.Цепочка приостановки продолжается вплоть до цикла событий, который замечает, что сопрограмма приостановлена, удаляет ее из рабочего набора и продолжает выполнять сопрограммы, которые запускаются, если таковые имеются. Если сопрограммы не запускаются, цикл ожидает,
select()
пока дескриптор файла, который интересует сопрограмму, не станет готовым для ввода-вывода. (Цикл событий поддерживает отображение дескриптора файла в сопрограмму.)В приведенном выше примере, как только
select()
сообщается, что цикл событий доступен дляsock
чтения, он будет повторно добавленcoro
к запускаемому набору, поэтому он будет продолжен с точки приостановки.Другими словами:
По умолчанию все происходит в одном потоке.
Цикл событий отвечает за планирование сопрограмм и их пробуждение, когда то, чего они ждали (обычно вызов ввода-вывода, который обычно блокируется, или тайм-аут) становится готовым.
Чтобы получить представление о циклах событий, управляющих сопрограммами, я рекомендую этот доклад Дейва Бизли, в котором он демонстрирует кодирование цикла событий с нуля перед живой аудиторией.
источник
async.wait_for()
не делает то, что должен ... Почему такая большая проблема - добавить обратный вызов в цикл событий и сообщить об этом обработать необходимое количество обратных вызовов, в том числе только что добавленный? Мое разочарованиеasyncio
отчасти вызвано тем фактом, что основная концепция очень проста, и, например, Emacs Lisp имел реализацию на протяжении многих лет, без использования модных словечек ... (то естьcreate-async-process
иaccept-process-output
- и это все, что нужно ... (продолжение)wait_for
оно не выполняет то, что должно (оно работает, это сопрограмма, которую вы должны ждать), а в том, что ваши ожидания не соответствуют тому, для чего была разработана и реализована система. Я думаю, ваша проблема могла бы быть сопоставлена с asyncio, если бы цикл событий выполнялся в отдельном потоке, но я не знаю подробностей вашего варианта использования, и, честно говоря, ваше отношение не делает его очень интересным, чтобы помочь вам.My frustration with asyncio is in part due to the fact that the underlying concept is very simple, and, for example, Emacs Lisp had implementation for ages, without using buzzwords...
- Тогда ничто не мешает вам реализовать эту простую концепцию без модных словечек для Python :) Почему вы вообще используете этот уродливый asyncio? Реализуйте собственное с нуля. Например, вы можете начать с создания собственнойasync.wait_for()
функции, которая выполняет именно то, что от нее требуется.asyncio
. Но, в принципе, это не мое решение. Меня принуждают использовать язык мусора через en.wikipedia.org/wiki/Ultimatum_game .Все сводится к двум основным проблемам, которые решает asyncio:
Ответ на первый вопрос существует уже давно и называется циклом выбора . В python это реализовано в модуле селекторов .
Второй вопрос связан с концепцией сопрограммы , то есть функций, которые могут останавливать свое выполнение и впоследствии восстанавливаться. В python сопрограммы реализуются с использованием генераторов и оператора yield from . Это то, что скрывается за синтаксисом async / await .
Дополнительные ресурсы в этом ответе .
РЕДАКТИРОВАТЬ: обращение к вашему комментарию о горутинах:
Ближайший эквивалент горутины в asyncio на самом деле не сопрограмма, а задача (см. Разницу в документации ). В python сопрограмма (или генератор) ничего не знает о концепциях цикла событий или ввода-вывода. Это просто функция, которая может остановить свое выполнение с
yield
сохранением текущего состояния, чтобы его можно было восстановить позже.yield from
Синтаксис позволяет для построения цепочки их прозрачным образом.Теперь в задаче asyncio сопрограмма в самом низу цепочки всегда возвращает будущее . Это будущее затем всплывает в цикл событий и интегрируется во внутренний механизм. Когда будущее установлено на выполнение каким-либо другим внутренним обратным вызовом, цикл событий может восстановить задачу, отправив будущее обратно в цепочку сопрограмм.
РЕДАКТИРОВАТЬ: отвечая на некоторые вопросы в вашем сообщении:
Нет, в ветке ничего не происходит. Ввод-вывод всегда управляется циклом событий, в основном через файловые дескрипторы. Однако регистрация этих файловых дескрипторов обычно скрыта сопрограммами высокого уровня, что делает грязную работу за вас.
Ввод / вывод - это любой блокирующий вызов. В asyncio все операции ввода-вывода должны проходить через цикл событий, потому что, как вы сказали, цикл событий не может знать, что блокирующий вызов выполняется в каком-то синхронном коде. Это означает, что вы не должны использовать синхронный
open
код в контексте сопрограммы. Вместо этого используйте специальную библиотеку, такую как aiofiles, которая предоставляет асинхронную версиюopen
.источник
yield from
, на самом деле ничего не говорит.yield from
это просто синтаксическая конструкция, а не фундаментальный строительный блок, который могут выполнять компьютеры. Аналогично для цикла выбора. Да, сопрограммы в Go также используют цикл выбора, но то, что я пытался сделать, будет работать в Go, но не в Python. Мне нужны более подробные ответы, чтобы понять, почему это не сработало.asyncio
для меня действительно, будет сводиться к коду C, который иллюстрирует, во что был переведен синтаксис Python.