29. Multithreading with QThreadPool and QRunnable

The QThreadPool class manages a collection of QThreads and is used with the QRunnable class.

29.1 A Minimal Example

An icon of a clipboard-list1

You need to provide a working multithreading application using QThreadPool with QRunnable.

 1 from PySide6.QtCore import QObject, QRunnable, Signal
 2 
 3 
 4 class Signals(QObject):
 5     progress = Signal(str)
 6     error = Signal(str)
 7 
 8 # 1. Create a QRunnable subclass
 9 #    and implement its run() method.
10 
11 class Runnable(QRunnable):
12     
13     def __init__(self, parent=None):
14         super().__init__(parent)
15         self.signals = Signals()
16     
17     # The run method will be executed
18     # in the worker thread.
19     
20     def run(self):
21         self.signals.progress.emit('Progress emitted')
22         print('Hello World')
23         self.signals.deleteLater()
  1. Create a QRunnable subclass and implement its run() method with the code to execute in a background thread. QRunnable is not a QObject subclass, so it cannot have signals directly. You can easily work around this by creating a separate QObject subclass (named Signals here) and adding an instance to your QRunnable. In this example, run() emits Signals.progress and prints a message.
 1 import sys
 2 
 3 from PySide6.QtCore import QThreadPool, Slot, Qt
 4 from PySide6.QtWidgets import (QApplication,
 5     QPushButton, QLabel, QWidget, QVBoxLayout)
 6 from runnable import Runnable
 7 
 8 
 9 class Window(QWidget):
10     
11     def __init__(self):
12 
13         super().__init__()
14         
15         layout = QVBoxLayout()
16         self.setLayout(layout)
17         
18         button = QPushButton('Start background thread')
19         button.clicked.connect(self.on_button_clicked)
20         
21         self.label = QLabel()
22         self.label.setAlignment(Qt.AlignmentFlag.AlignCenter)
23         
24         layout.addWidget(button)
25         layout.addWidget(self.label)
26     
27     # When the button is clicked:
28     
29     @Slot()
30     def on_button_clicked(self):
31         
32         # 2. Create a Runnable object
33         
34         runnable = Runnable()
35 
36         runnable.signals.progress.connect(self.label.setText)
37         runnable.signals.error.connect(self.on_error)
38         
39         # 3. Access the QThreadPool global instance
40         #    and run the task. 
41 
42         QThreadPool.globalInstance().start(runnable)
43     
44     @Slot()
45     def on_error(self, message):
46         print(message)
47 
48 
49 if __name__ == '__main__':
50 
51     app = QApplication(sys.argv)
52 
53     main_window = Window()
54     main_window.show()
55 
56     sys.exit(app.exec())
  1. In the main window, create a Runnable instance.

  2. Use QThreadPool.globalInstance() to access the global thread pool and QThreadPool.start() to execute Runnable.run() in one of its threads.

29.2 Walking the Filesystem

An icon of a clipboard-list1

Your task is to walk the filesystem using QThreadPool and QRunnable.

 1 import os
 2 from PySide6.QtCore import (QObject, QThread,
 3     QRunnable, Signal, Slot)
 4 
 5 
 6 class Signals(QObject):
 7     progress = Signal(str)
 8     error = Signal(str)
 9 
10 # 1. Create a QRunnable subclass
11 #    and implement its run() method
12 
13 class Runnable(QRunnable):
14     
15     def __init__(self, parent=None):
16         super().__init__(parent)
17         self.signals = Signals()
18         self.do_work = True
19     
20     # Enumerate fs objects while self.do_work flag is True
21     
22     def run(self):
23         path = os.path.abspath('.').split(os.path.sep)[0] + os.path.sep
24         for root, _, _ in os.walk(path):
25             if not self.do_work:
26                 return
27             self.signals.progress.emit(os.path.basename(root))
28             print(QThread.currentThread())
29     
30     @Slot()
31     def on_cancel_emitted(self):
32         self.do_work = False
  1. Create a QRunnable subclass and implement run(). Add a member for signals and a boolean flag do_work for interruption. run() uses os.walk() to enumerate filesystem objects, emitting progress() for each while do_work is True.
 1 import sys
 2 from PySide6.QtCore import QThreadPool, Slot, Signal, Qt
 3 from PySide6.QtWidgets import (QApplication,
 4     QPushButton, QLabel, QWidget, QVBoxLayout)
 5 from runnable import Runnable
 6 
 7 
 8 class Window(QWidget):
 9     
10     # 2. add a custom signal to be emitted
11     #    when we want to cancel the task. 
12     
13     cancel_runnable = Signal()
14     
15     def __init__(self):
16 
17         super().__init__()
18         
19         layout = QVBoxLayout()
20         self.setLayout(layout)
21         
22         self.start_button = QPushButton('Start background thread')
23         self.start_button.clicked.connect(self.on_start_button_clicked)
24         
25         self.cancel_button = QPushButton('Cancel')
26         self.cancel_button.clicked.connect(self.on_cancel_button_clicked)
27         self.cancel_button.setDisabled(True)
28         
29         self.label = QLabel()
30         self.label.setAlignment(Qt.AlignmentFlag.AlignCenter)
31         
32         layout.addWidget(self.start_button)
33         layout.addWidget(self.cancel_button)
34         layout.addWidget(self.label)
35     
36     @Slot()
37     def on_start_button_clicked(self):
38         
39         # 3. Create a Runnable object
40         #    and connect the signals and the slots
41         
42         runnable = Runnable()
43         
44         runnable.signals.progress.connect(self.label.setText)
45         runnable.signals.error.connect(self.on_error)
46         self.cancel_runnable.connect(runnable.on_cancel_emitted)
47         
48         # 4. Run the task.
49         
50         QThreadPool.globalInstance().start(runnable)
51         
52         self.start_button.setDisabled(True)
53         self.cancel_button.setEnabled(True)
54         
55     @Slot()
56     def on_cancel_button_clicked(self):
57         self.cancel_runnable.emit()
58         self.start_button.setEnabled(True)
59         self.cancel_button.setDisabled(True)
60     
61     @Slot()
62     def on_error(self, message):
63         print(message)
64     
65     # Emit the cancel_runnable signal to interrupt
66     # the runnable on the main window close 
67 
68     def closeEvent(self, event):        
69         try:
70             self.cancel_runnable.emit()
71         except Exception as e:
72             print(e) 
73 
74 
75 if __name__ == '__main__':
76 
77     app = QApplication(sys.argv)
78 
79     main_window = Window()
80     main_window.show()
81 
82     sys.exit(app.exec())
  1. In the main window class, add a custom cancel_runnable() signal. Connect it to Runnable.on_cancel_emitted(), which sets Runnable.do_work to False.

  2. In on_start_button_clicked() create a Runnable object and connects signals to slots: progress updates the label; on cancel, emit cancel_runnable() to set do_work False.

  3. Access the global thread pool and run the task with QThreadPool.start().