6

The code below works the way I expect it to work, namely:

  • there is a QThread ("Ernie") that counts from 1 to 8, sleeping for 1 second between counts
  • there is a gratuitous UI widget ("Bert")
  • under normal operation, the program runs until both the thread finishes and the UI is closed
  • a Ctrl-C keyboard interrupt will stop the program gracefully prior to normal completion.

To do this, I had to break up the 1-second sleep into 50 millisecond chunks that check a flag.

Is there a more Pythonic way to sleep in a thread for some amount of time (e.g. 1 second) but be interruptable by some flag or signal?

        try:
            for i in xrange(8):
                print "i=%d" % i
                for _ in xrange(20):
                    time.sleep(0.05)
                    if not self.running:
                        raise GracefulShutdown
        except GracefulShutdown:
            print "ernie exiting"        

I'd rather do this, and somehow cause a GracefulShutdown exception in the thread:

        try:
            for i in xrange(8):
                print "i=%d" % i
                time.sleep(1)
                # somehow allow another thread to raise GracefulShutdown
                # during the sleep() call
        except GracefulShutdown:
            print "ernie exiting"        

full program;

from PySide import QtCore, QtGui
from PySide.QtGui import QApplication
import sys
import signal
import time

class GracefulShutdown(Exception):
    pass

class Ernie(QtCore.QThread):
    def __init__(self):
        super(Ernie, self).__init__()
        self.running = True
    def run(self):
        try:
            for i in xrange(8):
                print "i=%d" % i
                for _ in xrange(20):
                    time.sleep(0.05)
                    if not self.running:
                        raise GracefulShutdown
        except GracefulShutdown:
            print "ernie exiting"        
    def shutdown(self):
        print "ernie received request to shutdown"
        self.running = False

class Bert(object):
    def __init__(self, argv):
        self.app = QApplication(argv)
        self.app.quitOnLastWindowClosed = False
    def show(self):
        widg = QtGui.QWidget()
        widg.resize(250, 150)
        widg.setWindowTitle('Simple')
        widg.show()
        self.widg = widg
        return widg
    def shutdown(self):
        print "bert exiting"
        self.widg.close()
    def start(self):
        # return control to the Python interpreter briefly every 100 msec
        timer = QtCore.QTimer()
        timer.start(100)
        timer.timeout.connect(lambda: None) 
        return self.app.exec_()        

def handleInterrupts(*actors):
    def handler(sig, frame):
        print "caught interrupt"
        for actor in actors:
            actor.shutdown()
    signal.signal(signal.SIGINT, handler)

bert = Bert(sys.argv)
gratuitousWidget = bert.show()
ernie = Ernie()
ernie.start()

handleInterrupts(bert, ernie)

retval = bert.start()
print "bert finished"
while not ernie.wait(100):
    # return control to the Python interpreter briefly every 100 msec
    pass
print "ernie finished"
sys.exit(retval)
Jason S
  • 184,598
  • 164
  • 608
  • 970

3 Answers3

8

I am not sure how Pythonic it is but it works. Just use a queue and use blocking get with a timeout. See the example below:

import threading
import Queue
import time

q = Queue.Queue()


def workit():
    for i in range(10):
        try:
            q.get(timeout=1)
            print '%s: Was interrupted' % time.time()
            break
        except Queue.Empty:
            print '%s: One second passed' % time.time()


th = threading.Thread(target=workit)
th.start()

time.sleep(3.2)
q.put(None)
Eugen
  • 2,292
  • 3
  • 29
  • 43
3

Normally, a SIGINT will interrupt a time.sleep call, but Python will only allow signals to received by the main thread of an application, so it can't be used here. I would recommend avoiding time.sleep if possible, and utilize a QTimer instead.

from PySide import QtCore, QtGui
from PySide.QtCore import QTimer
from PySide.QtGui import QApplication
import sys 
import signal
from functools import partial

class Ernie(QtCore.QThread):
    def __init__(self):
        super(Ernie, self).__init__()

    def do_print(self, cur_num, max_num):
        print "i=%d" % cur_num
        cur_num += 1
        if cur_num < max_num:
            func = partial(self.do_print, cur_num, max_num)
            QTimer.singleShot(1000, func)
        else:
            self.exit()

    def run(self):
        self.do_print(0, 8)
        self.exec_()  # QTimer requires the event loop for the thread be running.
        print "ernie exiting"    


class Bert(object):
    def __init__(self, argv):
        self.app = QApplication(argv)
        self.app.quitOnLastWindowClosed = False
    def show(self):
        widg = QtGui.QWidget()
        widg.resize(250, 150)
        widg.setWindowTitle('Simple')
        widg.show()
        self.widg = widg
        return widg
    def shutdown(self):
        print "bert exiting"
        self.widg.close()
    def start(self):
        # return control to the Python interpreter briefly every 100 msec
        timer = QtCore.QTimer()
        timer.start(100)
        timer.timeout.connect(lambda: None) 
        return self.app.exec_()            

def handleInterrupts(*actors):
    def handler(sig, frame):
        print "caught interrupt"
        for actor in actors:
            actor.shutdown()
    signal.signal(signal.SIGINT, handler)

bert = Bert(sys.argv)
gratuitousWidget = bert.show()
ernie = Ernie()
ernie.start()

handleInterrupts(bert)

retval = bert.start()
print "bert finished"
ernie.exit()
while not ernie.wait(100):
    # return control to the Python interpreter briefly every 100 msec
    pass
print "ernie finished"
sys.exit(retval)

Instead of having the run() method run a for loop with time.sleep, we start an event loop inside the thread, and use QTimer to do the desired printing at a set interval. This way, we can call bernie.exit() whenever we want the thread to shutdown, which will result in the bernie's event loop immediately shutting down.

Edit:

Here's an alternative way to implement this same idea that at least hides some of the complexity away, allowing the original for loop to be kept intact:

def coroutine(func):
    def wrapper(*args, **kwargs):
        def execute(gen):
            try:
                op = gen.next() # run func until a yield is hit
                # Determine when to resume execution of the coroutine.
                # If func didn't yield anything, schedule it to run again
                # immediately by setting timeout to 0.
                timeout = op or 0 
                func = partial(execute, gen)
                QTimer.singleShot(timeout, func) # This schedules execute to run until the next yield after `timeout` milliseconds.
            except StopIteration:
                return

        gen = func(*args, **kwargs) # Get a generator object for the decorated function.
        execute(gen) 
    return wrapper

def async_sleep(timeout):
    """ When yielded inside a coroutine, triggers a `timeout` length sleep. """
    return timeout

class Ernie(QtCore.QThread):
    def __init__(self):
        super(Ernie, self).__init__()
        self.cur_num = 0 
        self.max_num = 8 

    @coroutine
    def do_print(self):
        for i in range(8):
            print "i=%d" % i 
            yield async_sleep(1000) # This could also just be yield 1000
        self.exit()

    def run(self):
        self.do_print() # Schedules do_print to run once self.exec_() is run.
        self.exec_()
        print "ernie exiting"    

The coroutine allows the decorated function to give control back to the Qt event loop for a given amount of time whenever a yield appears, and then resumes execution of the decorated method. Granted, this is really just shifting complexity from my original example, but it does hide it away from the real work you're trying to do in the thread.

How it works:

The approach is inspired by the coroutine implementations in asynchronous libraries such as Tornado and the asyncio module. While I didn't try to come up with something as robust as those, the idea is the same. Methods which we want to be able to interrupt are implemented as generators, and decorated with a decorator that knows how to call and receive responses from the generator in a way that allows pausing/resuming the generator properly. The flow when do_print is called is basically this:

  1. do_print() is called from run. This actually results in coroutine.wrapper being called.
  2. wrapper calls the real do_print, which returns a generator object. It passes that object to execute.
  3. execute calls next on the generator object. This results in do_print running until a yield is hit. Execution of do_print is then suspended.
  4. execute schedules do_print to resume execution. It does this by first determining when to schedule it, by either using the value yielded from the previous iteration of do_print that ran, or by defaulting to 0 (which schedules execution to immediately resume). It calls QTimer.singleShot to schedule itself to run again in timeout milliseconds, using a partial so it can pass the generator object along, too.
  5. Steps 3-4 repeat until do_print stops yielding, calls self.exit() and returns, at which point StopIteration is raised, and the coroutine decorator simply returns rather than scheduling another execute call.
dano
  • 91,354
  • 19
  • 222
  • 219
  • I get your point but the implementation details seem to make this overly complex. I wouldn't bother with a thread at all if I could just deal with cooperative multiplexing or a state machine approach... but it makes the design simple to understand. – Jason S Jul 07 '14 at 19:59
  • @JasonS The QT project's website actually explicitly states that using threads along with a `sleep` for implementing timers is a [bad idea](https://qt-project.org/wiki/ThreadsEventsQObjects#80c8e93605b398d5fd0833250323bc89). They basically advocate using `QTimer` without bring in a `QThread` at all. So for your specific example, I think the preferred way would be to avoid `QThread` altogether. Of course, if you're doing something CPU intensive, that changes things. – dano Jul 07 '14 at 20:06
  • OK, the "sleep()" call here is really a proxy for any type of long operation which is not CPU bound *and* cannot easily be turned inside-out into an event-driven approach: waiting for network data, waiting for I/O, etc. in a complicated sequence of events. Yeah, I could use a QTimer and a state machine, but the state machine approach makes it a real horrid mess to design/test/review/etc. – Jason S Jul 07 '14 at 20:13
  • So yeah, from a concurrency point of view, the QTimer approach would be better, but the domain-specific priorities trump this. – Jason S Jul 07 '14 at 20:15
  • @JasonS Fair enough. I've edited my answer to include a coroutine-based approach that might fit your requirements a little bit better, since it shifts most of the complexity of using timers away from the main logic in the thread, and into a decorator. – dano Jul 07 '14 at 21:34
  • wow -- I need to take a look at that and figure out what's going on. Looks really interesting! – Jason S Jul 07 '14 at 22:30
  • @JasonS I've added further explanation of what's going on there. Hopefully it helps guide your understanding. – dano Jul 07 '14 at 23:12
1

My instinct would be to raise a signal with os.kill, but only the main thread ever receives signals, so Ernie can't be interrupted that way. The documentation suggests using locks instead.

My thoughts here would be to create a lock that can only be accessed when it's time to kill Ernie. After the main thread creates both Bert and Ernie, a lock file is created and locked. Then, instead of sleeping for one second, Ernie will spend one full second attempting to get the lock. Once it's time for the program to close, you can release the lock, which Ernie will immediately get; this tells Ernie that it's time to shut down.

Since you can't integrate signals and threads the way we'd like, here's another post talking about lock timeouts with threads:

How to implement a Lock with a timeout in Python 2.7

I couldn't tell you how Pythonic this solution is, since I'm still trying to get a grasp on what exactly being Pythonic entails. Once you start introducing threads, elegant code gets harder and harder to write, in any case.

Community
  • 1
  • 1
TheSoundDefense
  • 6,753
  • 1
  • 30
  • 42