3

The following code requires 3 presses of CTRL-C to end, how can I make it end with one only? (So it works nicely in Docker)

import asyncio
import time


def sleep_blocking():
    print("Sleep blocking")
    time.sleep(1000)


async def main():
    loop = asyncio.get_event_loop()
    await loop.run_in_executor(None, sleep_blocking)


try:
    asyncio.run(main())
except KeyboardInterrupt:
    print("Nicely shutting down ...")

I've read many asyncio related questions and answers but can't figure this one out yet. The 1st CTRL-C does nothing, the 2nd prints "Nicely shutting down ..." and then hangs. The 3rd CTRL-C prints an ugly error.

I'm on Python 3.9.10 and Linux.

(edit: updated code per comment @mkrieger1)

Otto
  • 1,787
  • 1
  • 17
  • 25
  • 1
    What happens if you replace `loop.run_until_complete(loop.run_in_executor(...))` by `await loop.run_in_executor(...)`? – mkrieger1 Feb 15 '22 at 20:06
  • 1
    @mkrieger1 then it requires 3 CTRL-Cs :) Really. The 1st doesn't do anything it seems, the 2nd prints `"Nicely shutting down ..."` and the 3rd the ugly error again – Otto Feb 15 '22 at 20:13

2 Answers2

1

From here we know that it's effectively impossible to kill a task running in a thread executor. If I replace the default thread executor with a ProcessPoolExecutor, I get the behavior you're looking for. Here's the code:

import concurrent.futures
import asyncio
import time


def sleep_blocking():
    print("Sleep blocking")
    time.sleep(1000)


async def main():
    loop = asyncio.get_event_loop()
    x = concurrent.futures.ProcessPoolExecutor()
    await loop.run_in_executor(x, sleep_blocking)


try:
    asyncio.run(main())
except KeyboardInterrupt:
    print("Nicely shutting down ...")

And the result is:

$ python asynctest.py
Sleep blocking
^CNicely shutting down ...
larsks
  • 277,717
  • 41
  • 399
  • 399
  • Thanks, that works. But ... my non-contrived real code actually was also using `asyncio.run_coroutine_threadsafe` which doesn't work from another process. – Otto Feb 15 '22 at 21:01
  • You may want to consider updating your question with example code that reproduces the problem you're trying to solve. – larsks Feb 15 '22 at 23:09
1

The way to exit immediately and unconditionally from a Python program is by calling os._exit(). If your background threads are in the middle of doing something important this may not be wise. However the following program does what you asked (python 3.10, Windows10):

import asyncio
import time
import os


def sleep_blocking():
    print("Sleep blocking")
    time.sleep(1000)


async def main():
    loop = asyncio.get_event_loop()
    loop.run_until_complete(loop.run_in_executor(None, sleep_blocking))


try:
    asyncio.run(main())
except KeyboardInterrupt:
    print("Nicely shutting down ...")
    os._exit(42)
Paul Cornelius
  • 9,245
  • 1
  • 15
  • 24
  • Does the `42` mean anything specific other than "not 0"? – Jack Deeth Feb 16 '22 at 01:09
  • 1
    No, the function has to get an integer argument. So I picked a famous one. – Paul Cornelius Feb 16 '22 at 03:15
  • So simple .... great! Question, I see this in the `os` docs: `The standard way to exit is sys.exit(n). _exit() should normally only be used in the child process after a fork().` Any reason you recommend os._exit? – Otto Feb 16 '22 at 07:49
  • Answer to my question: `sys.exit(42)` doesn't end the program, it hangs and still requires multiple CTRL-Cs – Otto Feb 16 '22 at 07:54
  • Right. sys.exit() will block until all the secondary, non-daemon threads have closed. I couldn't see any way to mark your secondary threads as daemon since they are created by multiprocessing.Pool, and it doesn't give you that possibility. – Paul Cornelius Feb 16 '22 at 20:16