5

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

Community
  • 1
  • 1
Jon Cohen
  • 445
  • 3
  • 16

3 Answers3

3

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.

jazzpi
  • 1,399
  • 12
  • 18
  • This *almost* does the trick. However, it waits until the sleep at the end of the thread loop is over to exit the process. Putting that same try-except block around the sleep inside the Foo class doesn't work, I think because the KeyboardInterrupt occurs in the main thread. – Jon Cohen Apr 14 '15 at 20:20
  • Yes, the Exception is thrown in the main thread - you could replace the sleep with a simple `pass`, but that's going to put one of your CPU cores at 100%. There might be a more elegant solution for this, but I think a 0.1 second delay between doing `Ctrl+C` and the program ending is acceptable. – jazzpi Apr 14 '15 at 20:56
  • No, no, the sleep within the loop *inside Foo* has to finish, so in my particular case it's a 10 minute long wait. – Jon Cohen Apr 14 '15 at 20:58
2

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
Jon Cohen
  • 445
  • 3
  • 16
1

@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)

shx2
  • 61,779
  • 13
  • 130
  • 153