«Вогонь і забудь» асинхрон / очікування пітона


115

Іноді має місце якась некритична асинхронна операція, але я не хочу чекати її завершення. У реалізації програмного забезпечення Tornado ви можете "запустити і забути" асинхронну функцію, просто опустивши yieldключове слово.

Я намагався розібратися, як "запустити і забути" за допомогою нового async/ awaitсинтаксису, випущеного в Python 3.5. Наприклад, спрощений фрагмент коду:

async def async_foo():
    print("Do some stuff asynchronously here...")

def bar():
    async_foo()  # fire and forget "async_foo()"

bar()

Однак, що трапляється, це те, що він bar()ніколи не виконується, і замість цього ми отримуємо попередження про час виконання:

RuntimeWarning: coroutine 'async_foo' was never awaited
  async_foo()  # fire and forget "async_foo()"

Пов'язані? stackoverflow.com/q/32808893/1639625 Насправді, я думаю, що це дублікат, але я не хочу його моментально забивати. Чи може хтось підтвердити?
tobias_k

3
@tobias_k, я не думаю, що це дублікат. Відповідь за посиланням занадто широка, щоб відповісти на це питання.
Михайло Герасимов

2
Чи (1) ваш "основний" процес продовжує працювати назавжди? Або (2) ви хочете, щоб ваш процес помер, але дозволяючи забутим завданням продовжувати свою роботу? Або (3) ви віддаєте перевагу своєму головному процесу очікування забутих завдань безпосередньо перед його закінченням?
Жульєн Палард

Відповіді:


170

Оновлено:

Замінити asyncio.ensure_futureз asyncio.create_taskвсюди , якщо ви використовуєте Python> = 3,7 Це новий, гарний спосіб для спавна завдання .


asyncio. Завдання "пожежі та забуття"

Згідно з документами python для asyncio.Taskцього можна запустити якусь програму для виконання "у фоновому режимі" . Завдання, створене asyncio.ensure_future функцією , не заблокує виконання (тому функція повернеться негайно!). Це виглядає як спосіб "стріляти і забути", як ви просили.

import asyncio


async def async_foo():
    print("async_foo started")
    await asyncio.sleep(1)
    print("async_foo done")


async def main():
    asyncio.ensure_future(async_foo())  # fire and forget async_foo()

    # btw, you can also create tasks inside non-async funcs

    print('Do some actions 1')
    await asyncio.sleep(1)
    print('Do some actions 2')
    await asyncio.sleep(1)
    print('Do some actions 3')


if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())

Вихід:

Do some actions 1
async_foo started
Do some actions 2
async_foo done
Do some actions 3

Що робити, якщо завдання виконуються після завершення циклу подій?

Зауважте, що асинчіо очікує, що завдання буде виконане в момент завершення циклу подій. Тож якщо ви перейдете main()на:

async def main():
    asyncio.ensure_future(async_foo())  # fire and forget

    print('Do some actions 1')
    await asyncio.sleep(0.1)
    print('Do some actions 2')

Ви отримаєте це попередження після завершення програми:

Task was destroyed but it is pending!
task: <Task pending coro=<async_foo() running at [...]

Щоб запобігти тому, що ви можете просто очікувати всіх очікуваних завдань після завершення циклу подій:

async def main():
    asyncio.ensure_future(async_foo())  # fire and forget

    print('Do some actions 1')
    await asyncio.sleep(0.1)
    print('Do some actions 2')


if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())

    # Let's also finish all running tasks:
    pending = asyncio.Task.all_tasks()
    loop.run_until_complete(asyncio.gather(*pending))

Вбивайте завдання, а не чекайте їх

Іноді не хочеться чекати виконання завдань (наприклад, деякі завдання можуть бути створені для запуску назавжди). У такому випадку ви можете просто скасувати їх (), а не чекати їх:

import asyncio
from contextlib import suppress


async def echo_forever():
    while True:
        print("echo")
        await asyncio.sleep(1)


async def main():
    asyncio.ensure_future(echo_forever())  # fire and forget

    print('Do some actions 1')
    await asyncio.sleep(1)
    print('Do some actions 2')
    await asyncio.sleep(1)
    print('Do some actions 3')


if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())

    # Let's also cancel all running tasks:
    pending = asyncio.Task.all_tasks()
    for task in pending:
        task.cancel()
        # Now we should await task to execute it's cancellation.
        # Cancelled task raises asyncio.CancelledError that we can suppress:
        with suppress(asyncio.CancelledError):
            loop.run_until_complete(task)

Вихід:

Do some actions 1
echo
Do some actions 2
echo
Do some actions 3
echo

Я скопіював і пройшов перший блок і просто запустив його на своєму кінці, і чомусь я отримав: рядок 4 async def async_foo (): ^ Начебто є якась помилка синтаксису з визначенням функції у рядку 4: "async def async_foo ( ): "Я щось пропускаю?
Гіл Аллен

3
@GilAllen цей синтаксис працює лише в Python 3.5+. Python 3.4 потребує старого синтаксису (див. Docs.python.org/3.4/library/asyncio-task.html ). Python 3.3 і пізніше не підтримує асинціо.
Михайло Герасимов

Як би ви вбили завдання в потоці? ... У мене є потік, який створює деякі завдання, і я хочу знищити всі відкладені, коли нитка відмирає у своєму stop()методі.
Сардатріон - проти зловживань SE

@Sardathrion Я не впевнений, чи задано десь тему на тему, в якій вона створена, але ніщо не заважає вам відстежувати їх вручну: наприклад, просто додайте всі завдання, створені в потоці, до списку і, коли настане час, скасуйте їх так, як це пояснено вище.
Михайло Герасимов

2
Зауважте, що "Task.all_tasks () застарілий з Python 3.7, використовуйте замість asyncio.all_tasks ()"
Alexis

12

Дякую Сергію за вдалу відповідь. Ось оформлений варіант того ж.

import asyncio
import time

def fire_and_forget(f):
    def wrapped(*args, **kwargs):
        return asyncio.get_event_loop().run_in_executor(None, f, *args, *kwargs)

    return wrapped

@fire_and_forget
def foo():
    time.sleep(1)
    print("foo() completed")

print("Hello")
foo()
print("I didn't wait for foo()")

Виробляє

>>> Hello
>>> foo() started
>>> I didn't wait for foo()
>>> foo() completed

Примітка. Перевірте іншу відповідь, яка робить те саме, використовуючи прості нитки.


Я зазнав значного уповільнення після використання цього підходу, створюючи ~ 5 невеликих завдань з пожежі та забуття за секунду. Не використовуйте це у виробництві для тривалого завдання. Він з'їсть ваш процесор і пам'ять!
пір

10

Це не зовсім асинхронне виконання, але, можливо, run_in_executor () підходить саме вам.

def fire_and_forget(task, *args, **kwargs):
    loop = asyncio.get_event_loop()
    if callable(task):
        return loop.run_in_executor(None, task, *args, **kwargs)
    else:    
        raise TypeError('Task must be a callable')

def foo():
    #asynchronous stuff here


fire_and_forget(foo)

3
Приємна стисла відповідь. Варто зауважити, що за executorзамовчуванням дзвінок буде замовчуванням concurrent.futures.ThreadPoolExecutor.submit(). Я згадую, тому що створення ниток не є безкоштовним; вогонь і забуття 1000 разів на секунду, ймовірно, поставить велику напругу в управлінні нитками
Бред Соломон

Так. Я не прислухався до вашого попередження і зазнав значного уповільнення після використання цього підходу, створюючи ~ 5 невеликих завдань з пожежі та забуття за секунду. Не використовуйте це у виробництві для тривалого завдання. Він з'їсть ваш процесор і пам'ять!
пір

3

Чомусь, якщо ви не можете використовувати, asyncioто тут реалізація з використанням простих потоків. Перевірте інші мої відповіді та відповідь Сергія.

import threading

def fire_and_forget(f):
    def wrapped():
        threading.Thread(target=f).start()

    return wrapped

@fire_and_forget
def foo():
    time.sleep(1)
    print("foo() completed")

print("Hello")
foo()
print("I didn't wait for foo()")

Якщо нам потрібна лише ця функція fire_and_forget і нічого іншого від asyncio, чи все-таки краще використовувати asyncio? Які переваги?
пір
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.