0

I have a class Controller with a method job which I'd like to run at regular intervals using the schedule module. Further, I'd like to have several 'variations' of this job running on separate threads such that they are all can be gracefully interrupted using Cntrl+C. (I do not want to make the threads daemon threads and shut them down abruptly).

Here is what I have so far:

import schedule
import threading
import time
import signal
import sys

class Controller(object):
    def __init__(self, name="controller", interval=1):
        self.name = name
        self.interval = interval

    def job(self):
        print("My name is {}.".format(self.name))


class ThreadController(threading.Thread):
    def __init__(self, *args, **kwargs):
        super(ThreadController, self).__init__()
        self.controller = Controller(*args, **kwargs)
        self._stop = threading.Event()

    def stop(self):
        self._stop.set()

    def stopped(self):
        return self._stop.isSet()

    def run(self):
        schedule.every(self.controller.interval).seconds.do(self.controller.job)
        while not self.stopped():
            schedule.run_pending()

if __name__ == "__main__":
    controller1 = ThreadController(name="foo")
    controller2 = ThreadController(name="bar")

    try:
        controller1.start()
        controller2.start()
        time.sleep(1000)            # This ensures that the execution remains within the 'try' block (for a significant amount of time)
    except KeyboardInterrupt:
        controller1.stop()
        controller2.stop()

The program works, in that for the first 1000 seconds it will alternately print My name is foo. and My name is bar. until Cntrl+C is pressed.

To make the code remain within the try block, however, I am for the time being using time.sleep which is not an elegant solution. What I actually want is to 'wait' until Cntrl+C is pressed. Is there an elegant way to implement this?

(Another thing I tried is the following, after Capture keyboardinterrupt in Python without try-except:

if __name__ == "__main__":
    controller1 = ThreadController(name="foo")
    controller2 = ThreadController(name="bar")

    def signal_handler(signal, frame):
        print("Stopping threads and exiting...")
        controller1.stop()
        controller2.stop()
        sys.exit(0)

    signal.signal(signal.SIGINT, signal_handler)

    controller1.start()
    controller2.start()

but this seems not to work, as the program keeps printing after Cntrl+C is pressed).

Community
  • 1
  • 1
Kurt Peek
  • 52,165
  • 91
  • 301
  • 526

2 Answers2

1

Ctrl+C terminates you main thread and controller1 and controller2 are still running.

You may demonize them

controller1.daemon = True
controller2.daemon = True

before starting. But when you main thread starts these two it will exit and shut down them as well. So in order to keep it busy run a infinite loop in it

while True:
     sleep(0.1)
Alexey Smirnov
  • 2,573
  • 14
  • 20
  • I stated in the question that I didn't want to daemonize the threads, because doing so makes them stop abruptly. But the infinite loop does seem like an effective way to keep the main thread alive. – Kurt Peek Nov 03 '16 at 12:40
0

For the time being I'm going with an infinite loop like the one suggested by Alexey Smirnov. The implementation is slightly different and uses Python's signal:

import schedule
import threading
import time
import signal
import sys

class Controller(object):
    def __init__(self, name="controller", interval=1):
        self.name = name
        self.interval = interval

    def job(self):
        print("My name is {}.".format(self.name))


class ThreadController(threading.Thread):
    def __init__(self, *args, **kwargs):
        super(ThreadController, self).__init__()
        self.controller = Controller(*args, **kwargs)
        self._stop = threading.Event()

    def stop(self):
        self._stop.set()

    def stopped(self):
        return self._stop.isSet()

    def run(self):
        schedule.every(self.controller.interval).seconds.do(self.controller.job)
        while not self.stopped():
            schedule.run_pending()


def signal_handler(signum, frame):
    controller_threads = [thread for thread in threading.enumerate() if isinstance(thread, ThreadController)]
    for controller_thread in controller_threads:
        print("Stopping {}.".format(controller_thread))
        controller_thread.stop()
    sys.exit(1)

if __name__ == "__main__":
    controller1 = ThreadController(name="foo")
    controller2 = ThreadController(name="bar")

    signal.signal(signal.SIGINT, signal_handler)

    controller1.start()
    controller2.start()

    while True: time.sleep(0.1)         # Keep the main thread alive until interrupted

The advantage of not using daemon threads is that they are not abruptly, but gracefully shut down.

Community
  • 1
  • 1
Kurt Peek
  • 52,165
  • 91
  • 301
  • 526