28. Using a QThread subclass

Perhaps the most important thing to remember about QThread is that a QThread object does not represent an operating system thread - it is a thread manager. In practice, this means only the QThread.run() method executes in the new thread. All other QThread methods run in the thread that created the QThread object.

Another point when subclassing QThread is that a QThread subclass does not start its event loop unless you explicitly call QThread.exec() in your QThread.run() implementation.

28.1 A Minimal Example

There are two ways to use QThread to run a background task:

  • Create a worker object and move it to a background thread using QObject.moveToThread().
  • Create a QThread subclass and override its run() method.

The examples from the previous chapter used QObject.moveToThread(). Now let’s examine a minimal QThread subclass.

An icon of a clipboard-list1

You need to provide a working multithreading application by subclassing QThread.

 1 from PySide6.QtCore import QThread, Signal, Slot
 2 
 3 # 1. Create a QThread subclass
 4 #    and override its run() method.
 5 #    Add signals as needed.
 6 
 7 class WorkerThread(QThread):
 8     
 9     result_ready = Signal(str)
10     
11     def __init__(self, parent=None):
12         super().__init__(parent)
13         print('Init in', QThread.currentThread().objectName(),
14             ', Loop level', QThread.currentThread().loopLevel())
15         
16     def run(self):
17         
18         print('Running in', QThread.currentThread().objectName(),
19             ', Loop level', QThread.currentThread().loopLevel())
20 
21         result = 'Hello World'
22         print(result)
23         self.result_ready.emit(result)
  1. Create a QThread subclass (named WorkerThread in the example) and override its run() method. Add custom signals to the subclass and emit them from run() to communicate with the main thread.
 1 # https://doc.qt.io/qt-6/qthread.html
 2 
 3 import sys
 4 
 5 from PySide6.QtCore import QThread, Slot, Qt
 6 from PySide6.QtWidgets import (QApplication, QPushButton,
 7     QLabel, QWidget, QVBoxLayout)
 8 from worker_thread import WorkerThread
 9 
10 
11 class Window(QWidget):
12     
13     def __init__(self):
14 
15         super().__init__()
16         
17         QThread.currentThread().setObjectName('Main thread')
18         
19         layout = QVBoxLayout()
20         self.setLayout(layout)
21         
22         button = QPushButton('Start background thread')
23         button.clicked.connect(self.start_work_in_a_thread)
24         
25         self.label = QLabel()
26         self.label.setAlignment(Qt.AlignmentFlag.AlignCenter)
27         
28         layout.addWidget(button)
29         layout.addWidget(self.label)
30     
31     @Slot()
32     def start_work_in_a_thread(self):
33         
34         print('Button click in', QThread.currentThread().objectName(),
35             ', Loop level', QThread.currentThread().loopLevel())
36         
37         # 2. Create the WorkerThread object
38         
39         self.worker_thread = WorkerThread()
40         self.worker_thread.setObjectName('Worker thread')
41         
42         # 3. Connect the signals with the slots
43         
44         self.worker_thread.result_ready.connect(self.on_result_ready)
45         self.worker_thread.finished.connect(self.worker_thread.deleteLater)
46         
47         # 5. Start the worker thread
48         
49         self.worker_thread.start()
50     
51     # 4. Handle the worker thread signals
52     
53     @Slot()
54     def on_result_ready(self, result):
55         self.label.setText(result)
56 
57 
58 if __name__ == '__main__':
59 
60     app = QApplication(sys.argv)
61     main_window = Window()
62     main_window.show()
63 
64     sys.exit(app.exec())
  1. In your main window class, create a WorkerThread instance.

  2. Connect the WorkerThread signals to main window slots. Also connect WorkerThread.finished() to WorkerThread.deleteLater() for clean thread exit.

  3. Create slots in the main window to handle WorkerThread signals. Here, we set a QLabels text to “Hello, World” on WorkerThread.result_ready().

  4. Start the worker thread.

When creating a plain QThread object (as in moveToThread() examples) the sequence is:

  • Call QThread.start() from the main thread.
  • The background QThread emits started().
  • QThread.start() invokes QThread.run().
  • QThread.run() calls QThread.exec().
  • QThread.exec() enters the thread-local event loop, waiting for QThread.exit() or QThread.quit().
  • Call QThread.exit() or QThread.quit() to stop.
  • The event loop stops, emitting QThread.finished()
  • QObjects with QObject.deleteLater() are deleted.

(Remember that in moveToThread() examples, we connected QThread.started() to a worker slot for immediate execution; and worker finished() to QThread.quit() for thread completion.)

When subclassing QThread, no local event loop unless QThread.exec() is called in run(). Without it, classes that need an event loop (e.g., QTimer, QTcpSocket, QProcess) won’t work. However, the thread object will still be able to emit signals.

In this example, Window.start_work_in_a_thread() runs in the main thread (QThread.loopLevel() = 1, event loop active). WorkerThread.__init__() also runs in the main thread, while only WorkerThread.run() executes in the worker thread (QThread.loopLevel() = 0, no event loop).

28.2 Walking the Filesystem

An icon of a clipboard-list1

Now let’s walk the filesystem using a QThread subclass.

 1 import os
 2 from PySide6.QtCore import QThread, Signal
 3 
 4 # 1. Create a QThread subclass
 5 #    and subclass its run() method.
 6 #    Add signals as needed.
 7 
 8 class WorkerThread(QThread):
 9     
10     progress = Signal(str)
11     
12     def __init__(self, parent=None):
13         super().__init__(parent)
14         print('Init it', QThread.currentThread().objectName())
15         
16     def run(self):
17         
18         print('Running in: ',
19             QThread.currentThread().objectName())
20         print('event loop level: ',
21             QThread.currentThread().loopLevel())
22 
23         path = os.path.abspath('.').split(os.path.sep)[0] + os.path.sep
24         for root, _, _ in os.walk(path):
25             if QThread.currentThread().isInterruptionRequested():
26                 return
27             self.progress.emit(os.path.basename(root))
  1. Create a QThread subclass named WorkerThread and override run(). In run(), print the current thread object name to confirm execution in the worker thread, then use os.walk() for recursive traversal. Emit progress() with each object’s name as the argument.
 1 import sys
 2 from PySide6.QtCore import QThread, Slot, Qt
 3 from PySide6.QtWidgets import (QApplication, QPushButton,
 4     QLabel, QWidget, QVBoxLayout)
 5 from worker_thread import WorkerThread
 6 
 7 
 8 class Window(QWidget):
 9     
10     def __init__(self):
11 
12         super().__init__()
13         
14         QThread.currentThread().setObjectName('Main thread')
15         
16         layout = QVBoxLayout()
17         self.setLayout(layout)
18         
19         self.start_button = QPushButton('Start background thread')
20         self.start_button.clicked.connect(self.on_start_button_clicked)
21         
22         self.cancel_button = QPushButton('Cancel')
23         self.cancel_button.clicked.connect(self.on_cancel_button_clicked)
24         self.cancel_button.setDisabled(True)
25         
26         self.label = QLabel()
27         self.label.setAlignment(Qt.AlignmentFlag.AlignCenter)
28         
29         layout.addWidget(self.start_button)
30         layout.addWidget(self.cancel_button)
31         layout.addWidget(self.label)
32     
33     @Slot()
34     def on_start_button_clicked(self):
35         
36         print('Main thread loop level', 
37             QThread.currentThread().loopLevel())
38         
39         # 2. Create the WorkerThread object
40 
41         self.worker_thread = WorkerThread()
42         self.worker_thread.setObjectName('Worker thread')
43 
44         # 3. Connect the signals with the slots
45 
46         self.worker_thread.progress.connect(self.on_progress)
47         self.worker_thread.finished.connect(
48             self.worker_thread.deleteLater)
49         
50         self.start_button.setDisabled(True)
51         self.cancel_button.setEnabled(True)
52         
53         # 5. Start the worker thread
54         
55         self.worker_thread.start()
56     
57     # 4. Handle the signals
58     
59     @Slot()
60     def on_cancel_button_clicked(self):
61         
62         self.start_button.setEnabled(True)
63         self.cancel_button.setDisabled(True)
64         
65         if hasattr(self, 'worker_thread'):
66             self.worker_thread.requestInterruption()
67             self.worker_thread.wait()
68         
69     @Slot()
70     def on_progress(self, msg):
71         self.label.setText(msg)
72         
73     # Make sure the thread is destroyed
74     # when the main window is closed.
75     
76     def closeEvent(self, event):        
77         try:
78             self.worker_thread.requestInterruption()
79             self.worker_thread.wait()
80         except Exception as e:
81             print(e) 
82 
83 
84 if __name__ == '__main__':
85 
86     app = QApplication(sys.argv)
87     main_window = Window()
88     main_window.show()
89 
90     sys.exit(app.exec())
  1. On start button click, create a WorkerThread object and set its name to “Worker thread”.

  2. Connect signals to slots: WorkerThread.progress() to Window.on_progress() for label updates; WorkerThread.finished() to WorkerThread.deleteLater() for cleanup.

  3. Handle signals. On progress() update the label; On cancel request interruption with QThread.requestInterruption() and wait for finish.

  4. Start the worker thread on each start button click.

Override Qwidget.closeEvent() to ensure thread deletion on window close. Note that we didn’t need to call QThread.quit() but only QThread.wait() since no event loop runs.