29. 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-list

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):
14         super().__init__()
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     main_window = Window()
53     main_window.show()
54     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-list

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

 1 import os
 2 from PySide6.QtCore import (QObject, QThread,
 3     QRunnable, QMutex, QMutexLocker, Signal, Slot)
 4 
 5 
 6 class Signals(QObject):
 7     progress = Signal(str)
 8     error = Signal(str)
 9 
10 
11 # 1. Create a QRunnable subclass
12 #    and implement its run() method
13 
14 class Runnable(QRunnable):
15     
16     def __init__(self):
17         super().__init__()
18         self.signals = Signals()
19         self.mutex = QMutex()
20         self.do_work = True
21         
22     def is_working(self):
23         with QMutexLocker(self.mutex):
24             return self.do_work
25         
26     def stop(self):
27         with QMutexLocker(self.mutex):
28             self.do_work = False
29     
30     # Enumerate fs objects while self.do_work flag is True
31     
32     def run(self):
33         path = os.path.abspath('.').split(os.path.sep)[0] + os.path.sep
34         for root, _, _ in os.walk(path):
35             if not self.is_working():
36                 return
37             self.signals.progress.emit(os.path.basename(root))
38     
39     @Slot()
40     def on_cancel_emitted(self):
41         self.stop()
  1. Create a QRunnable subclass and implement run(). Add a member for signals and a boolean flag, do_work, for interruption, guarded by a QMutex since it’s written from the main thread and read from the pool worker thread. is_working() and stop() wrap the guarded reads and writes in a QMutexLocker. run() uses os.walk() to enumerate filesystem objects, emitting progress() for each while is_working() 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     main_window = Window()
79     main_window.show()
80     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().

In the last three chapters we worked hard to pull pieces of code out of sync running them concurrently with moveToThread(), custom QThread subclasses, and the QThreadPool/QRunnable combination. In the next chapter we will work hard to bring that code back into sync again.