pythonpyqtpyqt5qthreadqt-slot

Has `@pyqtSlot()` same effect on a nested function?


1. Intro

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:

  1. I've pushed the Worker() object to my_thread.
  2. When commanding 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.
  3. 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.

 

2. My question

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?

 

3. Sidenote: how to hook to the inner function

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)?


Solution

  • In Qt there are the following rules about what:

    1. If you call a callable directly it will run on the thread where it was called.

    2. 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.

    3. If the callable does not belong to a context this will be executed in the thread where it was indirectly called.

    4. 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.