Asyncio keyboard input that can be canceled

Viewed 1321

I'm trying to write a concurrent Python program using asyncio that also accepts keyboard input. The problem appears when I try to shut down my program. Since keyboard input is in the end done with sys.stdin.readline, that function only returns after I press ENTER, regardless if I stop() the event loop or cancel() the function's Future.

Is there any way to provide keyboard input with asyncio that can be canceled?

Here is my MWE. It will accept keyboard inputs for 1 second, then stop():

import asyncio
import sys

async def console_input_loop():
    while True:
        inp = await loop.run_in_executor(None, sys.stdin.readline)
        print(f"[{inp.strip()}]")

async def sleeper():
    await asyncio.sleep(1)
    print("stop")
    loop.stop()

loop = asyncio.get_event_loop()
loop.create_task(console_input_loop())
loop.create_task(sleeper())
loop.run_forever()
1 Answers

The problem is that the executor insists on ensuring that all running futures have completed by the time the program terminates. But in this case you actually want an "unclean" termination because there's no portable way of canceling an on-going read() or of accessing sys.stdin asynchronously.

Canceling the future has no effect because concurrent.futures.Future.cancel is a no-op once its callback has started executing. The best way to avoid the unwanted waiting is to avoid run_in_executor in the first place and just spawn your own thread:

async def ainput():
    loop = asyncio.get_event_loop()
    fut = loop.create_future()
    def _run():
        line = sys.stdin.readline()
        loop.call_soon_threadsafe(fut.set_result, line)
    threading.Thread(target=_run, daemon=True).start()
    return await fut

The thread is created manually and marked as "daemon", so no one will wait for it at program shutdown. As a result, a variant of the code that uses ainput instead of run_in_executor(sys.stdin.readline) terminates as expected:

async def console_input_loop():
    while True:
        inp = await ainput()
        print(f"[{inp.strip()}]")

# rest of the program unchanged
Related