Reputation: 129
I have a Python program running a (nested) loop which will run for a fairly long time, and I want the user to be able to pause and/or abort it with just pressing p and c, respectively.
I am running this in an IPython console, so I don't really have access to msvctr.getch
and I kinda want to keep it platform independent.
Obviously, input()
blocks, which is exactly what I do not want. So I tried threading, which works when used as intended, but when hitting CTRLC the thread does not stop. This is likely because any legitimate method to stop the thread (atexit, global variable or lambda stop_thread
) isn't executed because the thread blocks.
import threading
import queue
q = queue.SimpleQueue()
stop_thread = False
def handle_input(q, stopped):
s = ''
while not stopped():
s = input()
q.put(s)
thread = threading.Thread(target=handle_input,
args=[q, lambda: stop_thread])
thread.start()
for i in range(very_long_time):
#Do something time consuming
if not q.empty():
s = q.get_nowait()
if 'p' in s:
print('Paused...', end='\r')
s = s.replace('p', '')
while True:
if not q.empty():
s += q.get_nowait()
if 'p' in s or 'c' in s:
s = s.replace('p', '')
break
time.sleep(0.5)
if 'c' in s:
print('\rAborted training loop...' + ' '*50, end='\r')
s = s.replace('c', '')
stop_thread = True
# Another method of stopping the thread
# thread.__getattribute__('_tstate_lock').release()
# thread._stop()
# thread.join()
break
This works in principle, but breaks when interrupting.
The thread does not seem to stop, which poses a problem when running this again in the same console, because it does not even ask for user input then.
Additionally, this prints my 'c' or 'p' and a newline, which I can't get rid of, because IPython doesn't allow all ANSI escapes.
Is there a fix to my method, or even better, a cleaner alternative?
Upvotes: 0
Views: 78
Reputation: 24711
You can try using the keyboard
module, which (among other things) lets you bind event hooks to keyboard presses.
In this case, I would create a set of global variables/flags (say, paused
and abort
), initially set to False
, and then make some hotkeys for p and c respectively to toggle them:
paused = False
abort = False
def toggle_paused():
global paused
paused = not paused
def trigger_abort():
abort = True
keyboard.add_hotkey('p', toggle_paused())
keyboard.add_hotkey('c', trigger_abort())
And then change your loop to check for paused
and abort
on every iteration (assuming, that is, that each iteration is fairly quick). What you're already doing would more-or-less work - just remove the queues and threading stuff you've already set up (IIRC keyboard
's events run on their own threads anyway), de-indent the if
conditions, and change the conditions to if paused:
and if abort:
respectively.
You can also lace the rest of your code with things that look for pause
or abort
flags, so that your program can gracefully pause or exit at a convenient time for it. You can also extend the toggle_paused()
and trigger_abort()
to do whatever you need them to (e.g. have trigger_abort()
print "Trying to abort program (kill me if I'm not done in 5 seconds)"
or something.
Although, as @Tomerikoo suggested in a comment, creating the threat with the daemon=True
option is the best answer, if it's possible with the way your program is designed. If this is all your program does then using daemon threads wouldn't work, because your program would just quit immediately, but if this is a background operation then you can use a daemon thread to put it in the background where it won't obstruct the rest of the user's experience.
Upvotes: 1