"Aktifkan dan lupakan" python async / await

115

Terkadang ada beberapa operasi asinkron non-kritis yang perlu dilakukan, tetapi saya tidak ingin menunggu hingga selesai. Dalam implementasi coroutine Tornado, Anda dapat "mengaktifkan & melupakan" fungsi asinkron hanya dengan menghilangkan kata yieldkuncinya.

Saya sudah mencoba mencari cara untuk "mengaktifkan & melupakan" dengan sintaks async/ baru yang awaitdirilis di Python 3.5. Misalnya, cuplikan kode yang disederhanakan:

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

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

bar()

Apa yang terjadi adalah yang bar()tidak pernah mengeksekusi dan sebagai gantinya kita mendapatkan peringatan runtime:

RuntimeWarning: coroutine 'async_foo' was never awaited
  async_foo()  # fire and forget "async_foo()"
Mike N
sumber
Terkait? stackoverflow.com/q/32808893/1639625 Sebenarnya, menurut saya ini duplikat, tapi saya tidak ingin langsung menipu. Bisakah seseorang mengkonfirmasi?
tobias_k
3
@tobias_k, menurut saya itu bukan duplikat. Jawaban di tautan terlalu luas untuk menjadi jawaban atas pertanyaan ini.
Mikhail Gerasimov
2
Apakah (1) proses "utama" Anda terus berjalan selamanya? Atau (2) apakah Anda ingin membiarkan proses Anda mati tetapi membiarkan tugas yang terlupakan melanjutkan pekerjaan mereka? Atau (3) apakah Anda lebih suka proses utama menunggu tugas yang terlupakan sebelum diakhiri?
Julien Palard

Jawaban:

170

Pembaruan:

Ganti asyncio.ensure_futuredengan asyncio.create_taskeverywhere jika Anda menggunakan Python> = 3.7 Ini lebih baru, cara lebih bagus untuk menelurkan tugas .


asyncio.Tanyakan untuk "aktifkan dan lupakan"

Menurut dokumentasi python asyncio.Task, adalah mungkin untuk memulai beberapa coroutine untuk dieksekusi "di latar belakang" . Tugas yang dibuat oleh asyncio.ensure_future fungsi tidak akan memblokir eksekusi (oleh karena itu fungsi akan segera kembali!). Ini terlihat seperti cara untuk "menembak dan melupakan" seperti yang Anda minta.

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())

Keluaran:

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

Bagaimana jika tugas dijalankan setelah event loop selesai?

Perhatikan bahwa asyncio mengharapkan tugas akan diselesaikan pada saat loop acara selesai. Jadi, jika Anda akan berubah main()menjadi:

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')

Anda akan mendapatkan peringatan ini setelah program selesai:

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

Untuk mencegahnya, Anda bisa menunggu semua tugas yang tertunda setelah event loop selesai:

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))

Bunuh tugas alih-alih menunggu mereka

Terkadang Anda tidak ingin menunggu tugas diselesaikan (misalnya, beberapa tugas mungkin dibuat untuk dijalankan selamanya). Dalam hal ini, Anda bisa membatalkannya () daripada menunggunya:

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)

Keluaran:

Do some actions 1
echo
Do some actions 2
echo
Do some actions 3
echo
Mikhail Gerasimov
sumber
Saya menyalin dan melewati blok pertama dan menjalankannya di ujung saya dan untuk beberapa alasan saya mendapat: baris 4 async def async_foo (): ^ Seolah-olah ada beberapa kesalahan sintaks dengan definisi fungsi pada baris 4: "async def async_foo ( ): "Apakah saya melewatkan sesuatu?
Gil Allen
3
@GilAllen sintaks ini hanya berfungsi di Python 3.5+. Python 3.4 membutuhkan sintaks lama (lihat docs.python.org/3.4/library/asyncio-task.html ). Python 3.3 dan yang lebih lama tidak mendukung asyncio sama sekali.
Mikhail Gerasimov
Bagaimana Anda akan menghentikan tugas di utas?… ̣Saya memiliki utas yang membuat beberapa tugas dan saya ingin menghentikan semua tugas yang tertunda ketika utas mati dalam stop()metodenya.
Sardathrion - terhadap pelecehan SE
@Sardathrion Saya tidak yakin apakah titik tugas di suatu tempat di utas tempat ia dibuat, tetapi tidak ada yang menghentikan Anda untuk melacaknya secara manual: misalnya, cukup tambahkan semua tugas yang dibuat di utas ke daftar dan ketika waktunya tiba batalkan dengan cara dijelaskan atas.
Mikhail Gerasimov
2
Perhatikan bahwa "Task.all_tasks () tidak digunakan lagi sejak Python 3.7, gunakan asyncio.all_tasks () sebagai gantinya"
Alexis
12

Terima kasih Sergey atas jawaban yang berhasil. Ini adalah versi dekorasi yang sama.

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()")

Menghasilkan

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

Catatan: Periksa jawaban saya yang lain yang melakukan hal yang sama menggunakan utas biasa.

nehem
sumber
Saya mengalami perlambatan substansial setelah menggunakan pendekatan ini dengan membuat ~ 5 tugas api-dan-lupakan kecil per detik. Jangan gunakan ini dalam produksi untuk tugas yang berjalan lama. Itu akan memakan CPU dan memori Anda!
pir
10

Ini bukan sepenuhnya eksekusi asinkron, tetapi mungkin run_in_executor () cocok untuk Anda.

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)
Sergey Gornostaev
sumber
3
Jawaban singkat yang bagus. Perlu dicatat bahwa executordefault akan menelepon concurrent.futures.ThreadPoolExecutor.submit(). Saya sebutkan karena membuat utas tidaklah gratis; api-dan-melupakan 1000 kali per detik mungkin akan memberikan tekanan besar pada manajemen utas
Brad Solomon
Ya. Saya tidak mengindahkan peringatan Anda dan mengalami perlambatan substansial setelah menggunakan pendekatan ini, menciptakan ~ 5 tugas api-dan-lupakan kecil per detik. Jangan gunakan ini dalam produksi untuk tugas yang berjalan lama. Itu akan memakan CPU dan memori Anda!
pir
3

Untuk beberapa alasan jika Anda tidak dapat menggunakan asynciomaka berikut adalah implementasinya menggunakan thread biasa. Periksa jawaban saya yang lain dan jawaban Sergey juga.

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()")
nehem
sumber
Jika kita hanya membutuhkan fungsionalitas fire_and_forget ini dan tidak ada yang lain dari asyncio, apakah masih lebih baik menggunakan asyncio? Apa manfaatnya?
pir