3

In thread, I have a loop that reads input from the user console. The main thread is busy with a Tkinter mainloop(). How do I terminate this program?

while True:
    ln = sys.stdin.readline()
    try:
        ln = ln[:-1]  # Remove LF from line
        if len(ln)==0: continue  # Ignore blank lines
        ...and so on

The main thead calls startGUI() which contains a tk.mainloop() call. When I press the X close button on the window (this is Linux), Tkinter closes the window and mainloop() returns. I then try to close stdin hoping that sys.stdin will close and cause sys.stdin.readline() will terminate with a nice EOF allowing my stdinLoop thread terminate.

# Start up the GUI window
startGUI()  # Doesn't return until GUI window is closed, tk.mainloop is called here
#
# Wait for stdinLoop thread to finish
sys.stdin.close()  # Hopefully cause stdinTh to close
print("waiting for stdinTh to join")
stdinTh.join()
print("joined stdinTh")

The sys.stdin.realine() never returns after the sys.stdin.close(). (The stdinTh.join() was there to synchronize the closing.)

I think Python readline() is doing something clever (in something called NetCommand) that doesn't return cleanly when stdin is closed.

Does Python think it is evil to have both a Tkinter GUI and use stdin interactively?

I tried using sys.stdin.read(1), but is seems buffer up a line and returns the whole line -- rather than reading one byte/char as I thought a read(1) would.

Ribo
  • 3,363
  • 1
  • 29
  • 35
  • Python doesn't think it's evil, but it's certainly not how GUI programs are designed to run. Depending on how you start the GUI there may not even be a stdin for it to read from. What are you trying to accomplish with both a GUI and reading from stdin? – Bryan Oakley Aug 04 '17 at 21:15
  • @BryanOakley, true, not conventional, I use the console for typing 'debugging' commands to the app. The GUI is the real thing the (end) user would interact with. – Ribo Aug 04 '17 at 21:20

1 Answers1

7

Make the thread a daemon thread that will be terminated automatically

Start the stdin-reading thread with daemon=True. It will then automatically terminate when the main thread terminates. You don’t need to do anything with stdin explicitly. (You also don’t get a chance to clean up in the stdin-reading thread.) For example:

stdinTh = threading.Thread(target=stdinLoop, name="stdinTh")
stdinTh.daemon = True
stdinTh.start()

If you can’t or don’t want to use a daemon thread

sys.stdin.readline() eventually boils down to a blocking read() system call.

read() on stdin does not return when stdin is closed. I’m not sure why you expect it to. This is not Python-specific behavior. At least on my Linux/glibc system, the same happens in C.

You can break out of a blocking read() by sending a signal (such as SIGUSR1) to the blocked thread. In C, you could use pthread_kill() for that. Python does not provide an easy way to do this, and for good reason; but if you insist, you can do it with ctypes.

But a cleaner/safer approach is to use select.select to read from either stdin or an inter-thread communication pipe, whichever is available first:

import os, select, sys, threading, time

def printer_loop(quit_pipe):
    while True:
        sys.stdout.write("Say something: ")
        sys.stdout.flush()
        (readable, _, _) = select.select([sys.stdin, quit_pipe], [], [])
        if quit_pipe in readable:
            print("Our time is up!")
            break
        # This is not exactly right, because `sys.stdin` could become
        # ready for reading before there's a newline on there, so
        # `readline` could still block. Ideally you would do some
        # custom buffering here.
        line = sys.stdin.readline()
        print("You said: '%s' - well said!" % line.strip())

def main():
    print("Starting thread...")
    (pipe_read, pipe_write) = os.pipe()
    thread = threading.Thread(target=printer_loop, args=(pipe_read,))
    thread.start()
    time.sleep(5)
    print("Interrupting thread...")
    os.write(pipe_write, b'.')
    print("Joining thread...")
    thread.join()
    print("All done!...")

if __name__ == '__main__':
    main()

This is not portable to Windows, where you can’t select() on sys.stdin.

Ribo
  • 3,363
  • 1
  • 29
  • 35
Vasiliy Faronov
  • 11,840
  • 2
  • 38
  • 49
  • I thought read() would terminate when the pipe is closed because it would see an EOF, right? (I'll try the deamon=true and the select.select.) It is odd that read(1) returns a complete line where read(1) on a 'file' object (stdin is supposed to be a file object isn't it?) is supposed to return 1 byte/char. – Ribo Aug 05 '17 at 23:10
  • @Ribo “I thought read() would terminate when the pipe is closed because it would see an EOF” — as I said, this doesn’t happen for me even in C – Vasiliy Faronov Aug 05 '17 at 23:15
  • @Ribo “read(1) returns a complete line” — I’m not sure why; can’t reproduce; probably best to ask a separate question – Vasiliy Faronov Aug 05 '17 at 23:18
  • @Ribo and Faronov. The thread blocks at readline, as noted in the comments. Why is it marked correct? It seems like it is not correct. – DrM Jan 08 '20 at 20:35
  • @DrM It should not block in the case described by Ribo, at least not usually, because the console doesn’t send anything to your stdin until it has the entire line, i.e. until the user hits Enter. Perhaps your case is different? It may be best to ask a separate question. – Vasiliy Faronov Jan 09 '20 at 18:38
  • That sounds like it is blocking. Read blocks and the thread does not terminate until the "enter". I have the same case as described by Ribo. – DrM Jan 10 '20 at 20:06