2015-06-10 19 views
29

Tôi có một vòng lặp sự kiện chạy một số đồng tác vụ như một phần của công cụ dòng lệnh. Người dùng có thể làm gián đoạn công cụ bằng thông thường Ctrl + C, tại thời điểm đó tôi muốn xóa sạch chính xác sau vòng lặp sự kiện bị gián đoạn.Cách chính xác để dọn sạch sau vòng lặp sự kiện bị gián đoạn là gì?

Đây là những gì tôi đã thử.

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

Chạy này và nhấn Ctrl + C sản lượng:

$ 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]> 

Rõ ràng, tôi không dọn dẹp một cách chính xác. Tôi nghĩ có lẽ gọi cancel() về các nhiệm vụ sẽ là cách để làm điều đó.

Cách chính xác để làm sạch sau vòng lặp sự kiện bị gián đoạn là gì?

+0

Trong trường hợp quan trọng, tôi đang chạy Python 3.4.3 trên OS X 10.10.3. –

Trả lời

30

Khi bạn CTRL + C, vòng lặp sự kiện được dừng lại, vì vậy các cuộc gọi của bạn để t.cancel() không thực sự có hiệu lực. Đối với các tác vụ bị hủy, bạn cần phải khởi động lại vòng lặp.

Đây là cách bạn có thể xử lý nó:

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

Một khi chúng ta bắt KeyboardInterrupt, chúng ta gọi là tasks.cancel() và sau đó bắt đầu loop lên một lần nữa. run_forever sẽ thực sự thoát khỏi càng sớm càng tasks bị hủy (lưu ý rằng hủy Future trả về bởi asyncio.gather cũng hủy bỏ tất cả các Futures bên trong của nó), vì bị gián đoạn loop.run_until_complete gọi thêm một done_callback để tasks mà dừng vòng lặp. Vì vậy, khi chúng tôi hủy tasks, các cuộc gọi lại sẽ ngừng hoạt động và vòng lặp dừng lại. Tại thời điểm đó, chúng tôi gọi số tasks.exception, chỉ để tránh bị cảnh báo về việc không tìm nạp ngoại lệ từ _GatheringFuture.

+0

Ah, vì vậy không có gì xảy ra với một nhiệm vụ bên ngoài vòng lặp sự kiện, thậm chí không phải là hủy bỏ, phải không? Điều đó nghe như một quy tắc đơn giản cần ghi nhớ. –

+0

Chỉ cần thử điều này và có vẻ như hoạt động như được quảng cáo. Ngọt! Lưu ý phụ: Tôi cũng nhận thấy rằng nếu bạn chuyển 'return_exceptions = True' thành' gather() ', bạn có thể bỏ cuộc gọi đến' tasks.exception() 'vì các ngoại lệ được trả về dưới dạng kết quả. –

+0

@NickChammas Phải, vòng lặp phải chạy để hủy bỏ có hiệu lực [(như được ghi trong tài liệu)] (https://docs.python.org/3/library/asyncio-task.html#asyncio.Task .cancel). Và có, nói chung không có gì sẽ xảy ra với một 'asyncio.Task' trừ khi vòng lặp đang tích cực lái nó. Sử dụng 'return_exceptions = True' là một thủ thuật tốt, miễn là bạn ổn với ngoại lệ thực (ví dụ: một cái gì đó không phải là' CancelledError') được ném từ coroutines bọc của bạn không thực sự được nâng lên. – dano

2

Trừ khi bạn đang sử dụng Windows, hãy thiết lập trình xử lý tín hiệu dựa trên vòng lặp sự kiện cho SIGINT (và cả SIGTERM để bạn có thể chạy nó dưới dạng dịch vụ). Trong các trình xử lý này, bạn có thể thoát khỏi vòng lặp sự kiện ngay lập tức hoặc khởi tạo một số loại trình dọn dẹp và thoát ra sau.

Ví dụ trong tài liệu Python chính thức: https://docs.python.org/3.4/library/asyncio-eventloop.html#set-signal-handlers-for-sigint-and-sigterm

+0

Bạn có thể làm rõ cách thức hoạt động này và tại sao nó thích hợp hơn để chỉ đơn giản là chụp 'KeyboardInterrupt' như tôi đã làm trong ví dụ của tôi? Việc gián đoạn dường như hoạt động tốt. Làm sạch các nhiệm vụ còn lại phía sau có vẻ là vấn đề. Có phải vì vòng lặp sự kiện không tự xử lý ngắt? –

+0

Nói chung khi bạn có vòng lặp sự kiện, bạn nên xử lý tất cả các loại sự kiện trong vòng lặp sự kiện. Tôi không thể nói lý do tại sao KeyboardInterrupt sẽ có vấn đề cụ thể. Hãy xem xét mặc dù nó có thể làm gián đoạn cơ bản bất kỳ mã thực hiện trong vòng lặp sự kiện (nhưng tôi không thể nói rằng chắc chắn vì tôi không biết các chi tiết của thiết kế). –

+0

Cách hoạt động của nó dường như không được mô tả trong tài liệu. Tôi cho rằng đó là thủ thuật "tự ống", bạn nên nhìn vào nguồn Python nếu bạn muốn biết. –

6

Dựa trên câu trả lời khác và một số suy nghĩ tôi đến giải pháp tiện dụng này mà nên làm việc gần như tất cả các trường hợp sử dụng và không phụ thuộc vào bạn tự theo dõi các công việc mà cần phải được làm sạch lên trên 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() 

Đoạn mã trên sẽ có được tất cả các hiện nhiệm vụ từ vòng lặp sự kiện sử dụng asyncio.Task.all_tasks và đặt chúng trong một tương lai kết hợp duy nhất sử dụng asyncio.gather. Tất cả các tác vụ trong tương lai (tất cả đều đang chạy các tác vụ) sau đó sẽ bị hủy bằng phương thức .cancel() của tương lai. Sau đó, return_exceptions=True đảm bảo rằng tất cả các ngoại lệ đã nhận được asyncio.CancelledError được lưu trữ thay vì làm cho tương lai bị lỗi.

Mã trên sẽ ghi đè trình xử lý ngoại lệ mặc định để ngăn

Các vấn đề liên quan