4

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()
pppery
  • 3,731
  • 22
  • 33
  • 46
cxxl
  • 4,939
  • 3
  • 31
  • 52
  • You’ll have to avoid readline, given that it does exactly what you say, i.e. waits for enter, and you want your keyboard input not to wait for enter. What else have you tried. – DisappointedByUnaccountableMod Oct 21 '19 at 20:25
  • 1
    @barny: there are not many options. I tried `input()` as well as `msvcrt.getwch()`. Same effect. – cxxl Oct 21 '19 at 20:34

1 Answers1

5

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
user4815162342
  • 141,790
  • 18
  • 296
  • 355
  • 1
    This question has been asked so many times on this site and this is my favorite answer so far. Running the input in another thread seems to be the only way to truly abort the blocking readline(). – nupanick Jun 30 '22 at 19:19