1

Disclaimer: the import statements are within the functions, I know that this is uncommon. I am showing my whole program here by showing it function for function while telling my issue and my thinking. In reality, I am doing something different, I made this minimal example just for this Stackoverflow question. There are duplicate questions, but I did not find good answers in them since those only say "use multithreading" (e.g. this answer). This particular question concerns itself with how to use multithreading.

The Story: I am running a program in Python. Let's say that it is a while loop going on to infinity. It just runs happily. For example,

def job(threadname, q):
  from time import sleep
  c = 0
  while True:
    sleep(0.1) #slow the loop down 
    c += 1
    print(c)

What I want to be able to do is that it asynchronously detects a keypress on stdin and then interrupts execution, so that I can do whatever I want within the function it is interrupted in (or if I am running it with python3 -i program.py, to switch over to the REPL with all my modules loaded in, remember this is a minimal example in which I do not want to highlight such concerns too much).

My idea was: I have one function that asynchronously gets the keypress, sends it via a queue to the other thread and it works. So I extended the job function as such:

def job(threadname, q):
  from time import sleep
  c = 0
  while True:
    sleep(0.1) #slow the loop down 
    c += 1
    print(c)
    ch = q.get() #extension for multithreading
    handle_keypress(ch) #extension for handling keypresses

The code for handle_keypress(ch) is:

def handle_keypress(key):
  if (key == "q"):
    print("Quit thread")
    exit(0)
  elif (key == "s"):
    print("would you like to change the step size? This has not been implemented yet.")
  else:
    print("you pressed another key, how nice! Unfortunately, there are not anymore options available yet.")

In other words, not that interesting other than to showcase that I want to be able to do this.

At first the issue seemed to be in the job() function. The culprit is q.get(), which is hanging. However, it is hanging because my input thread for some reason is not asynchronous and blocks. I have no clue how to make it unblocked.

This is the function of my input thread:

def get_input(threadname, q):
  #get one character, this code is adapted from https://stackoverflow.com/questions/510357/python-read-a-single-character-from-the-user
  while True:
    import sys, tty, termios
    fd = sys.stdin.fileno()
    old_settings = termios.tcgetattr(fd)
    try:
        tty.setraw(sys.stdin.fileno())
        ch = sys.stdin.read(1)
    finally:
        termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
    q.put(ch)

It is obvious to me that sys.stdin.read(1) is blocking, but I don't know how to make it unblocked. In the state that it is now, I also cannot think of a way to handle the poison pill situation, which is why q.get() in the job() function is blocking.

I run the program by calling the following function:

def run_program():
  from threading import Thread
  from queue import Queue
  queue = Queue()
  thread1 = Thread( target=get_input, args=("Thread-1", queue) )
  thread2 = Thread( target=job, args=("Thread-2", queue) )

  thread1.start()
  thread2.start()
  thread1.join()
  thread2.join()

My Questions: is this how you would design a program to deal with asynchronous keypresses? If so, how do I make the get_input() function unblocked?

Melvin Roest
  • 1,392
  • 1
  • 15
  • 31
  • 1
    `get_input()` must be blocking, it has no other work when no keypresses. `job()` must not block, because it's worker which do some processing. To fix it you must use `queue.get(block=False)` and check if returned `ch` is not None. If it's None just do your work. – Sav Jun 11 '19 at 05:06

1 Answers1

0

Thanks to Sav I found a way to answer this question. In my opinion, his comment is the answer. So if he'll rewrite his comment. I'll accept his answer. For now, I will show what part of the code I changed in order to get a non-blocking implementation working:

def job(threadname, q):
  from queue import Empty
  from time import sleep
  c = 0
  while True:
    sleep(0.1) #slow the loop down 
    c += 1
    print(c)
    #Below is the changed part
    ch = None
    try:
      ch = q.get(block=False)
    except Empty:
      pass
    if ch is not None:
      handle_keypress(ch) 
Melvin Roest
  • 1,392
  • 1
  • 15
  • 31