27. Multithreading - moveToThread

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

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

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

27.1 Blocking the Qt GUI: How Not to Do It

An icon of a clipboard-list1

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 using a custom Qt signal named progress. This fails because the loop blocks the Qt event loop, preventing the signals from being emitted or events from being processed until the loop completes. Since 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 illustrates 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-list1

You need to create a minimal working Qt multithreading application.

  1. Create a QObject subclass containing the slot/method to execute 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, which could be emitted under error conditions during process() execution.

Then, in the main window class:

  1. Create a QThread object named background_thread (avoid naming it thread, as that name is already used). Use self to make it the member of the main window, ensuring it remains in scope while running.

  2. Create a Worker object and move it to background_thread using QObject.moveToThread(). Now, Worker.process() will execute in background_thread.

  3. Connect the appropriate signals and slots:

    • Connect background_thread.started() to worker.process(). This executes Worker.process() when the background thread starts.
    • Connect Worker.finished() to background_thread.quit(). This quits the background thread when Worker.process() returns.
    • Connect Worker.finished() to Worker.deleteLater() method. This deletes the worker object some time after it emits finished().
    • Connect background_thread.finished() to background_thread.deleteLater(). This deletes the background thread some time after it finishes.
  4. Start background_thread using QThread.start().

With these steps, process() runs upon thread start, the thread quits when the method returns, and both the worker and thread object are destroyed - all while keeping the GUI responsive.

27.3 Walking the Filesystem

The moveToThread() template provides a basic structure, but the worker does nothing but print a message. For a more practical example, let’s have Worker.process() traverse the filesystem using os.walk() from the root. This operation can be time-consuming and would block the GUI if run on the main thread.

An icon of a clipboard-list1

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

  1. Create the worker class. The method, process() uses Python’s os.walk() to traverse the filesystem. For each enumerated filesystem object, we emit a custom progress signal (added to Worker alongside finished and error). If the background thread receives an interruption request, we return from process(), triggering the signal-slot chain to stop and delete both the worker and the background thread. Note how in Worker.process(), we access the current (background) thread via the static QThread.currentThread() method.
  1. In the main window class, create the thread object.

  2. Create a Worker object and move it to the QThread using QObject.moveToThread().

  3. Use signals and slots to ensure proper creation and deletion: Starting the background thread triggers Worker.process(); QObject.finished() triggers both QThread.quit() and Worker.deleteLater(); QThread.finished() triggers QThread.deleteLater().

  4. Start the background thread. This occurs in Window.on_start_button_clicked() creating new QThread and Worker objects each time the start button is clicked.

We 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 prior examples a new background thread is created each time the task runs. However, the official QThread documentation example creates both thread and worker in the main class constructor. Let’s try to replicate that.

An icon of a clipboard-list1

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 class:

  1. Create the worker thread.

  2. Create the Worker object and move it to the the worker thread using 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 occur 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 to execute Worker.do_work().

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

27.5 Walking the Filesystem reusing the QThread Object

An icon of a clipboard-list1

You need to use Qt multithreading to walk the filesistem but you reuse the same worker thread.

  1. Create the worker class. There are several differences from the first walk filesystem example: We use a boolean flag Worker.interruption_requested instead of QThread.isInterruptionRequested(); add Worker.stop() and Worker.reset() to toggle the flag; guard each use with QMutexLOcker and QMutex for thread safety.
  1. In Controller.__init__() create the worker thread object.

  2. Create the worker object 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 on main window close.

But why did we use a mutex-guarded boolean flag? QThread.requestInterruption() is one-time: 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 QMutex and QMutexLocker are used.

27.6 Signals and Slots Across Threads

Connection Type Emitter Thread Receiver Thread Slot Invoked Slot Executed In Blocks Unique
Auto A A immediately when the signal is emitted A No
Auto A B when control returns to the event loop of the receiver’s thread B No
Direct A B immediately when the signal is emitted A No
Queued A B when control returns to the event loop of the receiver’s thread B No
Blocking Queued A B when control returns to the event loop of the receiver’s thread B No
Unique A A immediately when the signal is emitted A Yes
Unique A B when control returns to the event loop of the receiver’s thread B Yes