Wednesday, September 5, 2012

QThread Best Practices -- When a QThread isn't a thread part 2

Review...

In When a QThread isn't a thread, we learned some of the finickiness in how QThread needs to be used. QT has a concept of "thread affinity" -- what thread the QObject "lives" in. At construction, a QObject becomes tied to the current running thread. Naturally, this applies to QThread as well. As of course our new thread cannot be running during its own creation, the new QThread instance is created while another "parent thread" is running. Because of the thread affinity rules, it now lives in the parent thread. This association with the parent thread combined with how QT signals-and-slots work creates a problematic situation. When emitting a signal, QT is smart enough figure out based on the destination/source QObject's thread affinity whether the signal's connected slots should be called directly as methods (these QObjects share thread affinity) or whether QT should post to the associated destination QThread's event queue for processing in the correct context (these QObjects that don't share a thread affinity).

Put all this together, and you'll see a problem. Since a QThread's affinity is the thread that created it, slots executing within it (that is methods of your QThread) will execute in the QThread's parent thread's context when responding to signals. In other words, if you "post" to a QThread method via a signal, you're not doing work in that QThread as you'd probably expect. A QThread isn't like, say, a CWinThread where methods in your thread tend to be handling thread messages and run in your thread's context.

To illustrate, in the code below, doWork does not execute in the MyWorkerThread's context. The worker thread is effectively meaningless as the meat of the work is being done in the worker thread's parent thread.

class MyWorkerThread(QThread):
    def __init__(self):
        super(MyWorkerThread, self).__init__()
       
    def run(self):     
        self.timer= QTimer()
        print self.timer.thread()
        self.timer.setSingleShot(False)
        self.timer.timeout.connect(self.doWork)
        self.timer.start(1000)
        self.exec_()

    def doWork(self):
        print "Work!"

Solution 0.1

The solution I proposed was to do all your required work in some secondary QObject (worker below). You can then change that thread's affinity to the worker thread by calling the very handy "moveToThread" method (see __init__ below). Now that  both QObjects are associated with MyWorkerThread, the work done on the timer signal is done in MyWorkerThread's context.

class MyWorkerThread(QThread):
    """ I'm just going to setup the event loop and do 
        nothing else..."""
    def __init__(self, worker):
        super(QThread, self).__init__()
        worker.moveToThread(self)
        self.worker = worker
        
    def run(self):      
        self.timer= QTimer()
        print self.timer.thread()
        self.timer.setSingleShot(False)
        self.timer.timeout.connect(self.worker.doWork)
        self.timer.start(1000)
        self.exec_() 

There's a better way...

Since writing the original post, I've read several illuminating articles that discuss this topic. Specifically The Great QThread Mess and Using QThread Without Subclassing.In short, they describe the same problem I do and go on to describe the sad state of QThread documentation. See as it turns out you don't have to subclass QThread. 

I had been overriding run because I thought I needed to call self.exec_() directly to setup an event queue. It appeared It turns out that the default implementation of run gives you an event queue! So an even better solution is to do something that doesn't subclass QThread. Something like:

worker = Worker()
workerThread = QThread()
timer = QTimer()
timer.timeout.connect(worker.doWork)\
worker.moveToThread(workerThread)
timer.moveToThread(workerThread)
timer.start(1000) # fire in one second
workerThread.start()

We simply create two QObjects that will communicate via signals-and-slots, create a vanilla QThread, assign the thread affinity of the new objects to that QThread, and voila! All work will be done in the worker thread's context. Moreover, any signal emited from anywhere will be handled in the worker's thread context.

QThread with new hotness best practices

This turns out that this is a neat way to setup work. It creates a bit of a danger zone though. In my 0.1 solution, the QThread provided a nice container for holding objects that other thread's code shouldn't play with. Here though, everything is wide open in plain sight in client code.

Despite the awesomeness above, I tend to not want to have the object's I've just pushed into the new worker to hang around as now its code is potentially executing in two thread contexts and at the very least my "worker" is probably not thread-safe. For example, some work might  be getting done in the worker thread's context via a signal and I may accidentally call a method directly on worker or timer. That all being said, I still tend to want to control how the worker is created. I may need to construct it with specific parameters describing the work to be done. I might need to connect it to some signals I or it might emit to facilitate communication across threads.

As such, I've generally started to follow something like this pattern for my own safety and sanity
def startPeriodicWork(self, worker):
    """ Takes "ownership" of worker, returns
        QThread thats started """
    workerThread = QThread()   # Create the QThread
    timer = QTimer()           # Create a periodic timer signal
    timer.timeout.connect(worker.doWork)
    worker.moveToThread(workerThread)
    timer.moveToThread(workerThread)
    workerThread.timer = timer   # Stash away, take ownership
    workerThread.worker = worker  # Stash away,take ownership
    timer.start(1000) # fire in one second
    workerThread.start()
    return workerThread

Notice under the "moveToThread" calls we store timer and worker as attributes of our QThread. I find this a handy way to keep the objects alive so they're not garbage collected (maybe that's paranoia on my part, but they seem to represent a cycle with nothing named in the code pointing to them). So I like to stash them away in my QThread.

 In client code I would do something like:
# Create the worker
worker = Worker(foo, bar, "otherParam")
# Setup some signals/slots for communication
worker.workDone.connect(self.handleWorkDone)
self.newWork.connect(worker.handleNewWork)
# pass to function to get work going
self.workerThread = startPeriodicWork(worker)
# FORGET worker
# (notice worker isn't stored in "self" only the workerThread)
Notice we forget about worker once we've started it. If for some reason I need to change something about "worker" we can't talk to it directly. But that's ok. We've setup some useful signals/slots to communicate between us and the worker. Since the worker is associated with workerThread and our client QObject is presumably not, we can rely on QT's signals and slots to safely post the signal's between the two QObject's thread queues safely.

Conclusion

I could keep getting anal about more finicky details of this code and ways to improve, but this is just the basic sketch of what seems to be a sane pattern for working with QThreads. Once you get the hang of it and realize you don't and shouldn't subclass QThread, life becomes a lot more straightforward and easier. Simply put, never do work in QThread. You should almost never need to override run. For most use cases, setting up proper associations with a QObject to a QThread and using QT's signals/slots creates an extremely powerful way to do multithreaded programming. Just be careful not to let the QObjects you've pushed to your worker threads hang around as certain hilarity will almost definitely ensue.

1 comment:

  1. Hi, thanks for these explanations!

    Note that you should not create the reference cycle. It will be resolved by the garbage collector which can run indeterministically and lead to a source of bugs that is even harder to discover and reproduce. You will be safer by removing this cycle, thus increasing the probability that problems will show up during testing. If you need to ensure that the objects stay alive, and qt does not do the job, you have to create global objects. (I'm speaking from bad experience myself.)

    ReplyDelete