Spags
Spags

Reputation: 11

Python: Background processes with an interrupt

I've got an application that creates a GUI with buttons. Each button starts a function that controls a robot. I'm running the functions on a separate thread so that the UI running on the main thread doesn't get locked up. I want to have a "Stop" button, that sends a command to stop the robot, but also immediately interrupts the thread process.

Because the robot functions have big time delays and loop (almost) indefinitely, e.g.

i = 0
while i<100
    start motorA
    time.sleep(120)
    stop motorA
    reverse motorA
    time.sleep(120)
    stop motor A
    i += 1

I can't just poll for an interrupt being set to true, or it could be 120s before it actually stops (long enough for an operator to have hit more buttons and changed the interrupt flag back to false...)

My GUI event loop currently looks like this:

while True:
    event,values = window.read()
    if event == "Button 1":
       stop_all()
       x = threading.Thread(target=function1, daemon=True)
       x.start()
    
    if event == "Button 2":
       stop_all()
       x = threading.Thread(target=function2, daemon=True)
       x.start()
    
    if event == "Stop":
       stop_all()

Is there anything I can add to my stop_all() function to immediately interrupt whatever slow, looping function is running in thread "x", or do I need to look at using multiprocessing instead of threading?

Cheers

Upvotes: 0

Views: 546

Answers (2)

Spags
Spags

Reputation: 11

Multiprocessing rather than Threading was a much nicer solution. Instead of:

x = threading.Thread(target = function1, daemon=True)

I use:

x = multiprocessing.Process(target = function1, daemon=True)

My Stop function then just becomes:

def stop_all()
   Stop motor
   x.terminate()
   x.join()

I just had to start and then immediately stop function1 so that the multiprocess "x" was defined before I tried to define stop_all()

Upvotes: 1

Oli
Oli

Reputation: 2602

One way to do this is to use a threading.Lock, and to use threading.Lock.acquire with a timeout as a replacement for time.sleep.

When you try to acquire the lock with a timeout when the lock has already been acquired, it waits for the timeout to expire before returning False. This behaviour is similar to time.sleep, however if the lock is released during this "sleeping" time, then it returns True immediately. Similarly, if the lock has already been released when threading.Lock.acquire is called, then it returns True immediately.

The ThreadStopper class implemented below provides a neater interface for managing this use case for a lock.

The below example demonstrates how to use the ThreadStopper class. The code starts 4 threads, which print something every 5 seconds, and when you press the Enter key a random one is stopped immediately, even if currently "sleeping".

Note that each thread needs it's own ThreadStopper passed to it.

import threading
import time
import random


class ThreadStopper:
    def __init__(self):
        self.lock = threading.Lock()
        self.lock.acquire()
        self.alive = True

    def sleep(self, t):
        if self.lock.acquire(timeout=t):
            self.alive = False

    def stop(self):
        self.lock.release()
        self.alive = False


num_threads = 4
thread_stoppers = []
threads = []


def worker(ts, i):
    print(f'I am thread {i}')
    while ts.alive:
        print(f'Thread {i} looping')
        ts.sleep(5) # ThreadStopper.sleep, not time.sleep

    print(f'Thread {i} finished')


for i in range(num_threads):
    thread_stopper = ThreadStopper()
    # create the thread, passing the newly created ThreadStopper
    thread = threading.Thread(target=worker, args=(thread_stopper, i))
    thread.start()

    thread_stoppers.append(thread_stopper)
    threads.append(thread)

    time.sleep(0.5)

running = set(range(num_threads))
while running:
    input()  # wait for enter to be pressed
    thread_to_stop = random.choice(list(running))
    thread_stoppers[thread_to_stop].stop() # signal the ThreadStopper to stop
    running.remove(thread_to_stop)

for thread in threads:
    thread.join()

Upvotes: 0

Related Questions