Я использую этот код для создания http-запроса каждые 5 секунд.
async def do_request():
async with aiohttp.ClientSession() as session:
async with session.get('http://localhost:8000/') as resp:
print(resp.status)
return await resp.text()
Не нашел встроенного планировщика, поэтому я написал эту функцию (похожую на javascript):
async def set_interval(fn, seconds):
while True:
await fn()
await asyncio.sleep(seconds)
И вот как я это использую:
asyncio.ensure_future(set_interval(do_request, 5))
Код работает нормально, но у меня есть два требования:
1. Как я могу остановить set_interval после того, как он добавлен в цикл событий? (похож на javascript clearInterval()
)
2. Возможно ли, что set_interval вернет значение функции, которую он оборачивает? В этом примере я хочу текст ответа. Другой шаблон, который будет выполнять ту же задачу, будет принят.
- Как я могу остановить set_interval после того, как он добавлен в цикл событий? (похож на javascript
clearInterval()
)
Один из вариантов - отменить возвращенное задание:
# create_task is like ensure_future, but guarantees to return a task
task = loop.create_task(set_interval(do_request, 5))
...
task.cancel()
Это также отменит все set_interval
будущем set_interval
. Если вы не хотите этого и хотите, чтобы fn()
продолжала работать в фоновом режиме, используйте вместо этого await asyncio.shield(fn())
.
- Возможно ли, что
set_interval
вернет значение функции, которую он оборачивает? В этом примере я хочу текст ответа. Другой шаблон, который будет выполнять ту же задачу, будет принят.
Поскольку set_interval
выполняется в бесконечном цикле, он не может ничего вернуть - возврат завершил бы цикл.
Если вам нужны значения из функции, один из вариантов - set_interval
как генератор. Вызывающая сторона получит значения, используя async for
, что вполне читабельно, но также сильно отличается от JavaScript setInterval
:
async def iter_interval(fn, seconds):
while True:
yield await fn()
await asyncio.sleep(seconds)
Пример использования:
async def x():
print('x')
return time.time()
async def main():
async for obj in iter_interval(x, 1):
print('got', obj)
# or asyncio.get_event_loop().run_until_complete(main())
asyncio.run(main())
Другой подход заключается в том, чтобы на каждом проходе цикла транслировать сгенерированное значение в глобальное будущее, которого могут ожидать другие сопрограммы; что-то вроде:
next_value = None
async def set_interval(fn, seconds):
global next_value
loop = asyncio.get_event_loop()
while True:
next_value = loop.create_task(fn())
await next_value
await asyncio.sleep(seconds)
С использованием, как это:
# def x() as above
async def main():
asyncio.create_task(set_interval(x, 1))
while True:
await asyncio.sleep(0)
obj = await next_value
print('got', obj)
Приведенная выше простая реализация имеет серьезную проблему: после предоставления следующего значения next_value
не сразу заменяется новым Future
, а только после сна. Это означает, что main()
печатает "got" в тесном цикле, пока не прибудет новая timestamp. Это также означает, что удаление await asyncio.sleep(0)
фактически await asyncio.sleep(0)
бы его, потому что единственное await
в цикле никогда не приостанавливалось и set_interval
больше не получал бы шанс на запуск.
Это явно не предназначено. Мы бы хотели, чтобы цикл в main()
ожидал следующего значения, даже после получения начального значения. Для этого set_interval
должен быть немного умнее:
next_value = None
async def set_interval(fn, seconds):
global next_value
loop = asyncio.get_event_loop()
next_value = loop.create_task(fn())
await next_value
async def iteration_pass():
await asyncio.sleep(seconds)
return await fn()
while True:
next_value = loop.create_task(iteration_pass())
await next_value
Эта версия гарантирует, что next_value
назначается, как только ожидается предыдущий. Для этого используется вспомогательная сопрограмма iteration_pass
которая служит удобной задачей для помещения в next_value
до того, как fn()
будет фактически готова к запуску. При этом main()
может выглядеть так:
async def main():
asyncio.create_task(set_interval(x, 1))
await asyncio.sleep(0)
while True:
obj = await next_value
print('got', obj)
main()
больше не занята зацикливанием и имеет ожидаемый вывод ровно одной отметки времени в секунду. Однако нам все еще нужен начальный asyncio.sleep(0)
потому что next_value
просто недоступно, когда мы просто вызываем create_task(set_interval(...))
. Это связано с тем, что задача, запланированная с помощью create_task
запускается только после возврата в цикл событий. Отказ от sleep(0)
может привести к ошибке вдоль строк "нельзя NoneType
объекты типа NoneType
".
Чтобы решить эту проблему, set_interval
может быть разделен на обычный def
который планирует начальную итерацию цикла и немедленно инициализирует next_value
. Функция создает экземпляр и немедленно возвращает объект сопрограммы, который выполняет остальную часть работы.
next_value = None
def set_interval(fn, seconds):
global next_value
loop = asyncio.get_event_loop()
next_value = loop.create_task(fn())
async def iteration_pass():
await asyncio.sleep(seconds)
return await fn()
async def interval_loop():
global next_value
while True:
next_value = loop.create_task(iteration_pass())
await next_value
return interval_loop()
Теперь main()
можно записать очевидным образом:
async def main():
asyncio.create_task(set_interval(x, 1))
while True:
obj = await next_value
print('got', obj)
По сравнению с асинхронной итерацией, преимущество этого подхода состоит в том, что несколько слушателей могут наблюдать значения.
async for
, у меня также будет бесконечный цикл for, правильно (но я думаю, что это не должно быть проблемой, потому что это неблокирующий цикл)? Во-вторых, было бы очень полезно увидеть код использования глобального будущего, как вы объяснили.