0

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?

freginold
  • 3,946
  • 3
  • 13
  • 28
JustAGuy
  • 129
  • 1
  • 11
  • 2
    simply make your thread `daemon`... :) read [this answer](https://stackoverflow.com/a/190017/6045800) – Tomerikoo Jul 15 '19 at 20:21
  • @Tomerikoo I tried that aswell, but it doesn't do it for me. I think that may be because I run it in an IPython console? – JustAGuy Jul 15 '19 at 21:13

1 Answers1

1

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.

Green Cloak Guy
  • 23,793
  • 4
  • 33
  • 53
  • Can you explain more about why `daemon` does not fit here? (out of curiosity, I am not an expert). As I understand it, when labeling a Thread as daemon it will be automatically killed when the main is terminated. As I see it, it doesn't matter what that thread's purpose is... – Tomerikoo Jul 15 '19 at 21:04
  • The fact that *the daemon is killed when the main thread terminates* is exactly the problem. If your program's purpose is to do operation `A`, and your main thread's only action is to split off a daemon thread to do `A` on its behalf, then the main thread is going to stop immediately afterwards, killing the daemon in the process before it's done anything useful. You could creatively avoid this by using `Thread.join()`, but at that point why not just do everything in the same thread? – Green Cloak Guy Jul 15 '19 at 21:06
  • And just as a note, I wasn't aware of `keyboard` module, that looks powerful. So thanks for that. learned something new... :) – Tomerikoo Jul 15 '19 at 21:06
  • I can understand why that holds in the example you gave. But isn't in that case the thread is doing a background job? It is just meant to "listen" on keyboard inputs. So if the main finished, no point on keep listening... maybe I missed something in the code's pupose... – Tomerikoo Jul 15 '19 at 21:08
  • Upon looking twice at OP's code, yes, you're right. If you want to elaborate on the benefits of using a daemon thread, feel free to make another answer and take credit for that! – Green Cloak Guy Jul 15 '19 at 21:11
  • haha I wasn't chasing credit or trying to prove. I have just recently experimented with threads and processes in a big project, and you seem like you know better so was just curious to learn :) Thanks – Tomerikoo Jul 15 '19 at 21:14
  • Thanks for your answer, I am looking into the keyboard module. But as for the `daemon` part, I tried this, but it did not kill the thread. I am executing this in the Spyder IDE in an IPython console. Did I miss something? – JustAGuy Jul 15 '19 at 21:23
  • @JustAGuy I am currently experimenting with that on the standard Python Shell and for some reason it is not working also. I am guessing that it is because the file I am using is itself some kind of a thread of the shell window, so maybe you are experiencing something similar. Best way to check is run the script from cmd – Tomerikoo Jul 15 '19 at 21:30
  • Yes confirmed @JustAGuy, ran in cmd and it worked... Must be because you're inside an IDE which is itself the main thread so as long as it is open....the thread will keep running – Tomerikoo Jul 15 '19 at 21:34
  • Hmm then I'll be using keyboard. It seems to have everything I want and far more. Although I do want to use as few dependencies as possible, this seems fitting. Thanks! – JustAGuy Jul 15 '19 at 21:37
  • It works like a charm with `keyboard`, thank you very much! – JustAGuy Jul 15 '19 at 22:43