Search code examples
pythonmultithreadingloopsterminate

Elegant loop exit using input() with multithreading


I have a function in a program that is implemented by a for loop that repeats "count" times. I need to be able to interrupt the loop at any time by typing 'stop' in the console. I have implemented this using two threads - one thread initializes the function with the loop:

    def send_messages(count_msg: int, delay_msg: int):
        global stop_flag
        for _ in range(count_msg):
            if stop_flag:  # Boolean variable responsible for stopping the loop
                print('---Sending completed by the user [ОК]---')
                break
            message(messageText)  # Function responsible for sending a message
            time.sleep(delay_msg)
        if stop_flag:
            stop_flag = False  # Change the flag back so that the function can be called again

and the other thread initializes a function that waits for user input via input().

    def monitor_input():
        global stop_flag 
        user_input = input()
        if user_input == 'stop':
            stop_flag = True  # Change the flag to stop the sending function


send_thread = threading.Thread(target=send_messages, args=my_args)
stop_thread = threading.Thread(target=monitor_input)
send_threading.start()
stop_threading.start()

Everything works correctly, but with one exception: if the loop is not interrupted, but just waits for its completion, the function still waits for user input, and it is inconvenient to close the program, more precisely, the error appears: UnicodeDecodeError: 'utf-8' codec can't decode byte 0xff in position 0: invalid start byte

I wish I could fix this

I would like to fix this, rather for myself, since I have already found a solution using the msvcrt module, but it only works for Windows, and I would like to find a more “flexible” way to do this, since i'm just learning to program

I tried to do this with error handling:

    def monitor_input():
        global stop_flag
        try:
            user_input = input()
        except UnicodeDecodeError as e:
            user_input = ''
            print('Terminated by user [OK]')  
            sys.exit(1)
        if user_input == 'stop':
            stop_flag = True  # Change the flag to stop the sending function

This worked, but not very nicely, since the program has to be closed “twice”, that is, it does not terminate after pressing the Terminate button. As far as I understand, this problem is due to the fact that one thread is still active.

I understand that this is a very minor problem, but I would like to fix it and also learn something new, so I would be very grateful for your help!


Solution

  • As you discovered, threads and blocking I/O together can be a problem, since it's tricky to knock a blocked thread out of its blocking I/O call when you want to shut down the thread cleanly and exit the program.

    Therefore, let's avoid that problem by avoiding the use of threads entirely; instead of blocking inside input(), we'll block inside select(), which will return either when the user has entered a command on stdin, or when it is time to increment the counter.

    Caveat: this approach doesn't work under Windows, because Windows' implementation of stdin is, um, sub-optimal and doesn't allow you to select() on it.

    import select
    import sys
    import time
    
    i = 0
    next_counter_increment_time = time.time()
    while(i < 100):
      now = time.time()
      seconds_until_next_counter_increment = next_counter_increment_time-now
      if (seconds_until_next_counter_increment < 0):
         seconds_until_next_counter_increment = 0
      inReady, outReady, exRead = select.select([sys.stdin], [], [], seconds_until_next_counter_increment)
      if (sys.stdin in inReady):  # check if there is something ready for us on stdin
         try:
            user_input = input() # we know this won't block
            print("You typed:  [%s]" % user_input)
         except UnicodeDecodeError as e:
            user_input = ''
            print('Terminated by user [OK]')
            sys.exit(1)
         if user_input == 'stop':
            print("Stopping now, bye!")
            sys.exit(1)
    
      now = time.time()
      if (now >= next_counter_increment_time):
         i += 1
         print("Counter is now %i" % i)
         next_counter_increment_time = now + 1.0
    
    print("Counter got to 100, exiting!")