Pertanyaan Apa cara yang benar untuk membersihkan setelah loop acara yang terganggu?


Saya memiliki loop peristiwa yang menjalankan beberapa co-routine sebagai bagian dari alat baris perintah. Pengguna dapat menginterupsi alat dengan biasa Ctrl + C, pada titik mana saya ingin membersihkan dengan benar setelah loop acara yang terganggu.

Inilah yang saya coba.

import asyncio


@asyncio.coroutine
def shleepy_time(seconds):
    print("Shleeping for {s} seconds...".format(s=seconds))
    yield from asyncio.sleep(seconds)


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

    # Side note: Apparently, async() will be deprecated in 3.4.4.
    # See: https://docs.python.org/3.4/library/asyncio-task.html#asyncio.async
    tasks = [
        asyncio.async(shleepy_time(seconds=5)),
        asyncio.async(shleepy_time(seconds=10))
    ]

    try:
        loop.run_until_complete(asyncio.gather(*tasks))
    except KeyboardInterrupt as e:
        print("Caught keyboard interrupt. Canceling tasks...")

        # This doesn't seem to be the correct solution.
        for t in tasks:
            t.cancel()
    finally:
        loop.close()

Menjalankan ini dan memukul Ctrl + C hasil:

$ python3 asyncio-keyboardinterrupt-example.py 
Shleeping for 5 seconds...
Shleeping for 10 seconds...
^CCaught keyboard interrupt. Canceling tasks...
Task was destroyed but it is pending!
task: <Task pending coro=<shleepy_time() running at asyncio-keyboardinterrupt-example.py:7> wait_for=<Future cancelled> cb=[gather.<locals>._done_callback(1)() at /usr/local/Cellar/python3/3.4.3/Frameworks/Python.framework/Versions/3.4/lib/python3.4/asyncio/tasks.py:587]>
Task was destroyed but it is pending!
task: <Task pending coro=<shleepy_time() running at asyncio-keyboardinterrupt-example.py:7> wait_for=<Future cancelled> cb=[gather.<locals>._done_callback(0)() at /usr/local/Cellar/python3/3.4.3/Frameworks/Python.framework/Versions/3.4/lib/python3.4/asyncio/tasks.py:587]>

Jelas, saya tidak membersihkan dengan benar. Saya pikir mungkin menelepon cancel() pada tugas akan menjadi cara untuk melakukannya.

Apa cara yang benar untuk membersihkan setelah loop acara yang terganggu?


32
2018-06-10 19:30


asal


Jawaban:


Ketika Anda CTRL + C, loop acara akan berhenti, jadi panggilan Anda ke t.cancel() tidak benar-benar berlaku. Untuk tugas-tugas yang harus dibatalkan, Anda perlu memulai kembali loop lagi.

Inilah bagaimana Anda dapat mengatasinya:

import asyncio

@asyncio.coroutine
def shleepy_time(seconds):
    print("Shleeping for {s} seconds...".format(s=seconds))
    yield from asyncio.sleep(seconds)


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

    # Side note: Apparently, async() will be deprecated in 3.4.4.
    # See: https://docs.python.org/3.4/library/asyncio-task.html#asyncio.async
    tasks = asyncio.gather(
        asyncio.async(shleepy_time(seconds=5)),
        asyncio.async(shleepy_time(seconds=10))
    )

    try:
        loop.run_until_complete(tasks)
    except KeyboardInterrupt as e:
        print("Caught keyboard interrupt. Canceling tasks...")
        tasks.cancel()
        loop.run_forever()
        tasks.exception()
    finally:
        loop.close()

Begitu kita menangkap KeyboardInterrupt, Kami memanggil tasks.cancel() dan kemudian mulai loop naik lagi. run_forever akan benar-benar keluar secepatnya tasks dibatalkan (perhatikan bahwa membatalkan Future dikembalikan oleh asyncio.gather juga membatalkan semua Futures di dalamnya), karena terganggu loop.run_until_complete panggilan ditambahkan a done_callback untuk tasks yang menghentikan loop. Jadi, saat kami batalkan tasks, callback itu diaktifkan, dan loop berhenti. Pada saat itu kami memanggil tasks.exception, hanya untuk menghindari peringatan tentang tidak mengambil pengecualian dari _GatheringFuture.


34
2018-06-10 19:58



Berdasarkan jawaban lain dan beberapa pemikiran saya tiba di solusi yang berguna ini yang seharusnya dapat digunakan hampir semua kasus penggunaan dan tidak bergantung pada Anda secara manual melacak tugas yang perlu dibersihkan pada Ctrl+C:

loop = asyncio.get_event_loop()
try:
    # Here `amain(loop)` is the core coroutine that may spawn any
    # number of tasks
    sys.exit(loop.run_until_complete(amain(loop)))
except KeyboardInterrupt:
    # Optionally show a message if the shutdown may take a while
    print("Attempting graceful shutdown, press Ctrl+C again to exit…", flush=True)

    # Do not show `asyncio.CancelledError` exceptions during shutdown
    # (a lot of these may be generated, skip this if you prefer to see them)
    def shutdown_exception_handler(loop, context):
        if "exception" not in context \
        or not isinstance(context["exception"], asyncio.CancelledError):
            loop.default_exception_handler(context)
    loop.set_exception_handler(shutdown_exception_handler)

    # Handle shutdown gracefully by waiting for all tasks to be cancelled
    tasks = asyncio.gather(*asyncio.Task.all_tasks(loop=loop), loop=loop, return_exceptions=True)
    tasks.add_done_callback(lambda t: loop.stop())
    tasks.cancel()

    # Keep the event loop running until it is either destroyed or all
    # tasks have really terminated
    while not tasks.done() and not loop.is_closed():
        loop.run_forever()
finally:
    loop.close()

Kode di atas akan mendapatkan semua tugas saat ini dari loop acara menggunakan asyncio.Task.all_tasks dan menempatkan mereka dalam satu masa depan gabungan menggunakan asyncio.gather. Semua tugas di masa depan itu (yang semuanya sedang berjalan) kemudian dibatalkan menggunakan masa depan .cancel() metode. Itu return_exceptions=True kemudian memastikan bahwa semua yang diterima asyncio.CancelledError pengecualian disimpan alih-alih menyebabkan masa depan menjadi error.

Kode di atas akan menggantikan penangan pengecualian default untuk mencegah


6
2018-02-07 18:40



Kecuali Anda berada di Windows, atur pengendali sinyal berbasis kejadian-loop untuk SIGINT (dan juga SIGTERM sehingga Anda dapat menjalankannya sebagai layanan). Dalam penangan ini, Anda dapat keluar dari lingkaran acara dengan segera, atau memulai semacam urutan pembersihan dan keluar kemudian.

Contoh dalam dokumentasi resmi Python: https://docs.python.org/3.4/library/asyncio-eventloop.html#set-signal-handlers-for-sigint-and-sigterm


2
2018-06-10 19:38