asyncio aiohttp отменить опрос HTTP-запроса, вернуть результат

1

Я использую этот код для создания 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 вернет значение функции, которую он оборачивает? В этом примере я хочу текст ответа. Другой шаблон, который будет выполнять ту же задачу, будет принят.

Теги:
python-asyncio
aiohttp
setinterval

1 ответ

3
Лучший ответ

Отмена работы

  1. Как я могу остановить 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()).

Распределение значений, сгенерированных функцией

  1. Возможно ли, что 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)

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

  • 0
    Спасибо! Используя эту async for , у меня также будет бесконечный цикл for, правильно (но я думаю, что это не должно быть проблемой, потому что это неблокирующий цикл)? Во-вторых, было бы очень полезно увидеть код использования глобального будущего, как вы объяснили.
  • 0
    @ user3599803 В случае генератора цикл находится как в вызывающей, так и в генераторе, причем значение передается на каждой итерации. И да, ожидание новой итерации (а также генерация нового значения) являются асинхронными и, следовательно, не блокируют цикл обработки событий.
Показать ещё 1 комментарий

Ещё вопросы

Сообщество Overcoder
Наверх
Меню