I'm working with PyQt5
in Python 3.7 on a multithreaded application, for which I rely on the QThread
.
Now suppose I have a class derived from QObject
. Within that class, I define a function annotated with @pyqtSlot
:
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
import threading
...
class Worker(QObject):
def __init__(self):
super().__init__()
return
@pyqtSlot()
def some_function(self):
...
return
In some other code, I instantiate Worker()
and move it to a new thread, like so:
my_thread = QThread()
my_worker = Worker()
my_worker.moveToThread(my_thread)
my_thread.start()
QTimer.singleShot(100, my_worker.some_function)
return
Normally, some_function()
should now run in my_thread
. That's because:
Worker()
object to my_thread
.my_thread
to start, I've actually given birth to a new Qt event-loop in that thread. The my_worker
object lives in this event-loop. All its slots can receive an event, which gets executed in this event-loop.some_function()
is properly annotated to be a @pyqtSlot()
. The single-shot-timer hooks onto this slot and fires an event. Thanks to the Qt-event-loop in my_thread
, the slot effectively executes its code in my_thread
.
My question is about nested functions (also called 'inner functions'). Consider this:
class Worker(QObject):
def __init__(self):
super().__init__()
return
def some_function(self):
...
@pyqtSlot()
def some_inner_function():
...
return
return
As you can see, some_inner_function()
is annotated as @pyqtSlot
. Will its code also run in the thread the Worker()
-object lives in?
You might wonder how I could hook something to the inner function. Well, consider the following:
class Worker(QObject):
def __init__(self):
super().__init__()
return
def some_function(self):
@pyqtSlot()
def some_inner_function():
# Will this code run in `my_thread`?
...
return
# some_function() will run in the main thread if
# it is called directly from the main thread.
QTimer.singleShot(100, some_inner_function)
return
If you call some_function()
directly from the main thread, it will (unfortunately) run in the main thread. Without properly using the signal-slot mechanism, you won't switch threads.
The single-shot-timer inside some_function()
hooks onto some_inner_function()
and fires. Will the inner function execute in my_thread
(supposing that the Worker()
-object was assigned to my_thread
)?
In Qt there are the following rules about what:
If you call a callable directly it will run on the thread where it was called.
If a callable is invoked indirectly (through qt signals, QTimer::singleShot()
or QMetaObject::invokeMethod()
) it will be executed in the context to which it belongs. And the context refers to the QObject.
If the callable does not belong to a context this will be executed in the thread where it was indirectly called.
The internal functions do not belong to a context, so even if it is called directly or indirectly, it will be executed in the thread where it was invoked.
Based on the above, let's analyze several cases as an exercise to verify the previous rules:
Example 1
from PyQt5 import QtCore
import threading
class Worker(QtCore.QObject):
def some_function(self):
def some_inner_function():
print("inner thread", threading.get_ident())
QtCore.QThread.sleep(1)
print("worker thread", threading.get_ident())
some_inner_function()
if __name__ == "__main__":
import sys
app = QtCore.QCoreApplication(sys.argv)
thread = QtCore.QThread()
thread.start()
my_worker = Worker()
my_worker.moveToThread(thread)
my_worker.some_function()
print("main thread", threading.get_ident())
sys.exit(app.exec_())
Output:
worker thread 140678349403776
inner thread 140678349403776
main thread 140678349403776
In this case rule 1 is fulfilled because all callables are called directly.
Example 2
from PyQt5 import QtCore
import threading
class Worker(QtCore.QObject):
def some_function(self):
@QtCore.pyqtSlot()
def some_inner_function():
print("inner thread", threading.get_ident())
QtCore.QThread.sleep(1)
print("worker thread", threading.get_ident())
QtCore.QTimer.singleShot(0, some_inner_function)
if __name__ == "__main__":
import sys
app = QtCore.QCoreApplication(sys.argv)
thread = QtCore.QThread()
thread.start()
my_worker = Worker()
my_worker.moveToThread(thread)
my_worker.some_function()
print("main thread", threading.get_ident())
sys.exit(app.exec_())
Output:
worker thread 139721158932096
main thread 139721158932096
inner thread 139721158932096
In this some function is called directly in the main thread so it will be executed in that thread, and since the some_inner_function is called by some_function then it will also be executed in that thread.
Example3:
from PyQt5 import QtCore
import threading
class Worker(QtCore.QObject):
def some_function(self):
@QtCore.pyqtSlot()
def some_inner_function():
print("inner thread", threading.get_ident())
QtCore.QThread.sleep(1)
print("worker thread", threading.get_ident())
QtCore.QTimer.singleShot(0, some_inner_function)
if __name__ == "__main__":
import sys
app = QtCore.QCoreApplication(sys.argv)
thread = QtCore.QThread()
thread.start()
my_worker = Worker()
my_worker.moveToThread(thread)
QtCore.QTimer.singleShot(0, my_worker.some_function)
print("main thread", threading.get_ident())
sys.exit(app.exec_())
Output:
main thread 139934436517504
worker thread 139934378075904
inner thread 139934378075904
In this case some_function is invoked indirectly and belongs to the Worker context so it will be executed on a secondary thread so some_inner_function will be executed on the secondary thread.
In conclusion some_inner_function
will run on the same thread as some_function
was executed, even call it directly or indirectly since it has no context.