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.
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
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.
Hi, thanks for these explanations!
ReplyDeleteNote 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.)
And 7 years and 3 months later, this is still a painful area to work in. I have found and read most of the "use worker thread" articles, but your two-part article is the clearest presentation of this information yet (at least to me). Thanks very much for writing this.
ReplyDeleteHi, got thanks to your posts here.
ReplyDeleteand and it almost kills me what is your pattern exactly.
I've followed your instructions build something like this:
```
import threading
import sys
from PyQt5.QtCore import (QCoreApplication, QObject, QRunnable, QThread,
QThreadPool, pyqtSignal, pyqtSlot, QTimer)
class Worker(QObject):
def __init__(self):
super(Worker, self).__init__()
# self.call_f1.connect(self.f1)
# self.call_f2.connect(self.f2)
call_f1 = pyqtSignal()
call_f2 = pyqtSignal()
@pyqtSlot()
def doWork(self):
'''
Must have '@pyqtSlot()', otherwise this will execute in main thread.
'''
#print('doWork', threading.get_ident())
print('doWork', self.thread())
@pyqtSlot()
def f1(self):
print('f1', threading.get_ident())
@pyqtSlot()
def f2(self):
print('f2', threading.get_ident())
class WorkLogic(QObject):
def __init__(self):
super(WorkLogic, self).__init__()
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)
#timer.start(1000) # fire at here would work, but i doubt it's thread affinity.
print('BEFORE', timer.thread())
worker.moveToThread(workerThread)
timer.moveToThread(workerThread)
print('AFTER', timer.thread())
print('WORKER', worker.thread())
workerThread.timer = timer # Stash away, take ownership
workerThread.worker = worker # Stash away,take ownership
timer.start(1000) # fire at here won't work
workerThread.start()
return workerThread
class MainCtrler(QObject):
def __init__(self):
super(MainCtrler, self).__init__()
def startProgram(self):
workLogic = WorkLogic()
# Create the worker
worker = Worker()
# Setup some signals/slots for communication
worker.call_f1.connect(worker.f1)
worker.call_f2.connect(worker.f2)
# pass to function to get work going
self.workerThread = workLogic.startPeriodicWork(worker)
# FORGET worker
# (notice worker isn't stored in "self" only the workerThread)
if __name__ == "__main__":
app = QCoreApplication([])
print('main', threading.get_ident())
print('MAIN', app.thread())
m_ctrl = MainCtrler()
m_ctrl.startProgram()
print('MAIN_CTRL', m_ctrl.thread())
sys.exit(app.exec_())
```
and it just won't work, pls help me.
looking forwarding to your reply !
#timer.start(1000) # fire at here would work, but i doubt it's thread affinity.
Deletehere i doubted the timer's thread affinity, and now i don't.
cause I tried sleep the main thread, and this still work !
and without moveToThread, it doesn't.
cause pyqt's doc said we can not start timer in another thread, i suggest we start the timer before moveToThread.