Jon Cohen
Jon Cohen

Reputation: 455

Interrupting a thread in Python with a KeyboardException in the main thread

I have a few classes that look more or less like this:

import threading
import time

class Foo():
    def __init__(self, interval, callbacks):
        self.thread = threading.Thread(target=self.loop)
        self.interval = interval
        self.thread_stop = threading.Event()
        self.callbacks = callbacks

    def loop():
        while not self.thread_stop.is_set():
            #do some stuff...
            for callback in self.callbacks():
                callback()
            time.sleep(self.interval)

    def start(self):
        self.thread.start()

    def kill(self):
        self.thread_stop.set()

Which I am using from my main thread like this:

interval = someinterval
callbacks = [some callbacks]

f = Foo(interval, callbacks)

try:
    f.start()
except KeyboardInterrupt:
    f.kill()
    raise

I would like a KeyboardInterrupt to kill the thread after all the callbacks have been completed, but before the loop repeats. Currently they are ignored and I have to resort to killing the terminal process that the program is running in.

I saw the idea of using threading.Event from this post, but it appears like I'm doing it incorrectly, and it's making working on this project a pretty large hassle.

I don't know if it may be relevant, but the callbacks I'm passing access data from the Internet and make heavy use of the retrying decorator to deal with unreliable connections.


EDIT

After everyone's help, the loop now looks like this inside Foo:

    def thread_loop(self):
        while not self.thread_stop.is_set():
            # do some stuff
            # call the callbacks
            self.thread_stop.wait(self.interval)

This is kind of a solution, although it isn't ideal. This code runs on PythonAnywhere and the price of the account is by CPU time. I'll have to see how much this uses over the course of a day with the constant waking and sleeping of threads, but it at least solves the main issue

Upvotes: 5

Views: 1428

Answers (3)

Jon Cohen
Jon Cohen

Reputation: 455

Thanks to @shx2 and @jazzpi for putting together the two separate pieces of the puzzle.

so the final code is

import threading
import time

class Foo():
    def __init__(self, interval, callbacks):
        self.thread = threading.Thread(target=self.loop)
        self.interval = interval
        self.thread_stop = threading.Event()
        self.callbacks = callbacks

    def loop():
        while not self.thread_stop.is_set():
            #do some stuff...
            for callback in self.callbacks():
                callback()
            self.thread_stop.wait(self.interval)

    def start(self):
        self.thread.start()

    def kill(self):
        self.thread_stop.set()

And then in main

interval = someinterval
callbacks = [some, callbacks]
f = Foo(interval, callbacks)
f.start()
try:
    while True:
        time.sleep(0.1)
except KeyboardInterrupt:
    f.kill()
    raise

Upvotes: 2

shx2
shx2

Reputation: 64318

@jazzpi's answer correctly addresses the issue you're having in the main thread.

As to the sleep in thread's loop, you can simply replace the call to sleep with a call to self.thread_stop.wait(self.interval).

This way, your thread wakes up as soon as the stop event is set, or after waiting (i.e. sleeping) for self.interval seconds. (Event docs)

Upvotes: 1

jazzpi
jazzpi

Reputation: 1429

I think your problem is that you have a try-except-block around f.start(), but that returns immediately, so you aren't going to catch KeyboardInterrupts after the thread was started.

You could try adding a while-loop at the bottom of your program like this:

f.start()
try:
    while True:
        time.sleep(0.1)
except KeyboardInterrupt:
    f.kill()
    raise

This isn't exactly the most elegant solution, but it should work.

Upvotes: 3

Related Questions