[Async-sig] Cancelling a coroutine from a signal handler?

Nathaniel Smith njs at pobox.com
Tue Apr 24 21:54:53 EDT 2018


On Tue, Apr 24, 2018 at 2:25 PM, Mark E. Haase <mehaase at gmail.com> wrote:
> My mental model of how the event loop works is pretty poor, but I roughly
> understand that the event loop is responsible for driving coroutines. It
> appears
> here that the event loop has stopped driving my main() coroutine, and so the
> only way to force it to complete is to call send() from my code.

It hasn't stopped driving your main() coroutine – as far as it knows,
main() is still waiting for the Event.wait() call to complete, and as
soon as it does it will start iterating the coroutine again.

You really, really, definitely should not be trying to manually
iterate a coroutine object associate with a Task.

> More broadly, handling KeyboardInterrupt in async code seems very tricky,
> but I also cannot figure out how to make this interrupt approach work. Is
> one
> of these better than the other? What is the best practice here? Would it be
> terrible to add `while True: main_coro.send(None)` to my signal handler?

Yes, it would be terrible :-).

Instead of trying to throw exceptions manually, you should call the
cancel() method on the Task object. (Of if you want to abort
immediately because the previous control-C was ignored, use something
like os._exit() or os.abort().)

The other complication is that doing *anything* from a signal handler
is fraught with peril, because of reentrancy issues. I actually don't
think there are *any* functions in asyncio that are guaranteed to be
safe to call from a signal handler. Looking at the code for
Task.cancel, I definitely don't trust that it's safe to call from a
signal handler.

The simplest solution would be to use asyncio's native signal handler
support instead of the signal module:
https://docs.python.org/3/library/asyncio-eventloop.html#unix-signals
However, there are some trade-offs:
- it's not implemented on Windows
- it relies on the event loop running. In particular, if the event
loop is stalled (e.g. because some task got stuck in an infinite
loop), then your signal handler will never be called, so your
"emergency abort" code won't work.

Alternatively, you can define a handler using signal.signal, and then
arrange to re-enter the asyncio main loop yourself before calling
Task.cancel. I believe that the only guaranteed-to-be-safe way to do
this is:

- in your signal handler, spawn a new thread (!)
- from the new thread, call loop.call_soon_threadsafe(your_main_task.cancel)

(Trio's version of call_soon_threadsafe *is* guaranteed to be both
thread- and signal-safe, but asyncio's isn't, and in asyncio there are
multiple event loop implementations so even if one happens to be
signal-safe by chance you don't know about the others... also Trio
handles control-C automatically so you don't need to worry about this
in the first place. But I don't know how to port Trio's generic
solution to asyncio :-(.)

-n

-- 
Nathaniel J. Smith -- https://vorpus.org


More information about the Async-sig mailing list