27. Multithreading - moveToThread

Threads of execution let you execute your code concurrently while sharing the program’s memory and other resources. There are two primary use cases for threads:

  • Accelerate processing by utilizing multiple processor cores,
  • Maintain GUI responsiveness by offloading long-running tasks to background threads.

In the following examples, we focus on the second use case. First, however, let’s demonstrate the problem by showing a non-responsive Qt GUI.

27.1 Blocking the Qt GUI: How Not to Do It

An icon of a clipboard-list

You need to search the filesystem for a file and report progress.

  1. Create the task. In the example, we use os.walk() within a for loop to search the file system for a non-existent file. We attempt to report progress after each iteration by emitting a custom Qt signal named progress(). This fails because the loop blocks the Qt event loop. Even though progress() is emitted and its slot run immediately, the window can’t repaint or respond to input until the loop returns control to the event loop. Because the loop runs in the main application thread (also known as the GUI thread), the entire application becomes unresponsive - it freezes and you cannot even close it.

  2. Create a push button.

  3. Create a slot that starts the long-running task when the push button is clicked.

27.2 A Minimal Working Example

The previous example shows how a PySide6 GUI can become unresponsive. Now let’s explore using Qt threads to run long tasks in the background while keeping the GUI responsive.

An icon of a clipboard-list

You need to create a minimal working Qt multithreading application.

  1. Create a QObject subclass that contains the slot/method to be executed in a background thread (i.e. a thread other than the GUI thread). In the example the slot is named process(). It prints a message and emits a custom signal named finished() before returning. The Worker class declares an error() signal that can be emitted if an error occurs during process() execution.

Then, in the main window class:

  1. Create a QThread object named background_thread (avoid naming it simply thread, as that name is already used - QObject.thread() which returns the thread in which the object lives). Store it as a member of the main window (self.background_thread) so that it does not go out of scope when on_button_clicked() returns.

  2. Create a Worker object and move it to background_thread with QObject.moveToThread(). From this point on, Worker.process() will execute in the background thread.

  3. Connect the appropriate signals and slots:

    • (2) background_thread.started() -> worker_obj.process(). This makes Worker.process() execute when the background thread starts.
    • (3) worker_obj.finished() -> background_thread.quit(). This stops the background thread event loop when Worker.process() returns.
    • (4) worker_obj.finished() -> worker_obj.deleteLater() method. This schedules the worker for deletion.
    • (5) background_thread.finished() -> background_thread.deleteLater(). This schedules the thread object for deletion.
  4. Start the background thread with QThread.start() (1).

With these connections, process() runs as soon as the thread starts, the thread quits when the method returns, and both the worker and thread object are cleaned up automatically, while the GUI remains responsive.

27.3 Walking the Filesystem

The moveToThread() template provides a basic structure, but the worker does nothing useful. For a more practical example, we make Worker.process() traverse the filesystem with os.walk() starting from the root. This can take a long time and would freeze the GUI if it ran on the main thread.

An icon of a clipboard-list

You need to use Qt multithreading to search the filesystem for a file and report progress.

  1. Create the worker class. The process() method uses Python’s os.walk() to traverse the filesystem. For each enumerated filesystem object, it emits a custom progress() signal (declared alongside finished() and error()). If the background thread receives an interruption request, process() returns early, which triggers the usual signal-slot chain that stops and deletes both the worker and the background thread. Note that inside process(), we obtain the current (background) thread with the static method QThread.currentThread().
  1. In the main window class, create the thread object.

  2. Create a Worker object and move it to the thread with QObject.moveToThread().

  3. Wire the lifetime signals exactly as in the minimal example:

    • thread started -> worker process,
    • worker finished -> thread quit,
    • worker finished -> worker delete later,
    • thread finished -> thread delete later.
  4. Start the background thread. In this example the start happens inside Window.on_start_button_clicked() so a new QThread and Worker are created each time the start button is pressed.

We also override QWidget.closeEvent() to interrupt the background thread, ensuring cleanup if the main window closes while the thread runs. The sequence, QThread.requestInterruption() + QThread.quit() + QThread.wait(), is also used in Window.on_cancel_button_clicked() for clean interruption.

27.4 Reusing the QThread object

In the previous examples a new background thread is created each time the task runs. The official QThread documentation example, however, creates both the thread and the worker once, in the main class constructor. Let’s reproduce that pattern.

An icon of a clipboard-list

Your task is to recreate the official Qt multithreading example in PySide6.

  1. Create the worker class. The background method is do_work(), matching the QThread documentation.

Then, in the main window (controller) class:

  1. Create the worker thread.

  2. Create the Worker and move it to the worker thread with QObject.moveToThread().

  3. Connect signals and slots:

    • QThread.finished() to QObject.deleteLater() for worker deletion on thread finish,
    • Controller.operate() to Worker.do_work() to start work via custom signal,
    • Worker.result_ready() to Controller.handle_result() for handling results.
  4. Start the worker thread. Steps 2-5 are performed in Controller.__init__(), so the thread and the worker persist until the main window closes or explicit deletion.

  5. On button click, emit the operate() signal. This causes Worker.do_work() to run.

  6. Reimplement QWidget.closeEvent() to quit the thread using QThread.quit() and QThread.wait().

27.5 Walking the Filesystem While Reusing the QThread

An icon of a clipboard-list

You need to walk the filesystemwith Qt multithreading while reusing the same worker thread.

  1. Create the worker class. Several differences from the earlier filesystem example appear here:
    • A boolean flag interruption_requested is used instead of QThread.isInterruptionRequested()
    • We add Worker.stop() and Worker.reset() methods to toggle the flag
    • Every access to the flag is protected by a QMutexLocker and QMutex for thread safety.
  1. In Controller.__init__() create the worker thread object.

  2. Create the worker and move it to the worker thread using QObject.moveToThread().

  3. Connect signals and slots. Main class operate() signal triggers Worker.do_work().

  4. Start the worker thread.

  5. On Start button click, reset the worker with Worker.reset() and emit operate().

  6. On Cancel button click, stop the worker with Worker.stop().

  7. Quit the thread when the main window closes.

But why did we use a mutex-guarded boolean flag? QThread.requestInterruption() is a one-shot mechanism: once requested, QThread.isInterruptionRequested() stays True and it cannot be reset. A signal to toggle a flag would require QApplication.processEvents() in the blocking loop. Direct flag setting is unsafe across threads, so a QMutex is required.

27.6 Signals and Slots Across Threads

Connection Type Threads Slot Invoked Executed In Blocks Emitter
Auto Same (A->A) immediately when emitted A No
Auto Different (A->B) when control returns to B’s event loop B No
Direct Same (A->A) immediately when emitted A No
Direct Different (A->B) immediately when emitted A No
Queued Same (A->A) when control returns to A’s own event loop A No
Queued Different (A->B) when control returns to B’s event loop B No
Blocking Queued Same (A->A) deadlocks - Deadlock
Blocking Queued Different (A->B) when control returns to B’s event loop B Yes

Any of the four connection types above can be combined with Qt.UniqueConnection (e.g., Qt.AutoConnection | Qt.UniqueConnection) to make connect() reject a duplicate connection.

Qt lets you write signal.connect(slot, Qt.ConnectionType.BlockingQueuedConnection) between two objects that live in the same thread, but doing so hangs that thread permanently. A blocking queued connection posts an event to the receiver’s event loop and then blocks the emitting thread until that event has been processed. When both sender and receiver are in the same thread, the event loop that would process it is the one you have just blocked. Qt detects the self-connection at runtime and prints a warning, but the call still blocks forever regardless. In the example above, the sender and receiver are in two different background threads, but if you commented these two lines:

1 # self.emitter.moveToThread(self.emitting_thread)
2 # self.receiver.moveToThread(self.receiving_thread)

you would get this message printed in the terminal before the application is blocked:

1 Qt: Dead lock detected while activating a BlockingQueuedConnection: Sender is Worker(0x17b9aed2c10), receiver is Worker(0x17b9aed2d30)

We deal with connection types in more detail in Chapter 32.