Walk the Filesystem While Reusing the Thread
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
# https://doc.qt.io/qt-6/qthread.html
import os
import sys
from PySide6.QtCore import (QObject, QThread,
Slot, Signal, QMutex, QMutexLocker, Qt)
from PySide6.QtWidgets import (QApplication,
QPushButton, QLabel, QWidget, QVBoxLayout)
# 1. Create the worker class
class Worker(QObject):
result_ready = Signal()
progress = Signal(str)
def __init__(self, parent=None):
super().__init__(parent)
self.interruption_requested = False
self.mutex = QMutex()
@Slot()
def do_work(self):
self.interruption_requested = False
path = os.path.abspath('.').split(os.path.sep)[0] + os.path.sep
for root, _, _ in os.walk(path):
with QMutexLocker(self.mutex):
if self.interruption_requested:
self.progress.emit('Canceled')
self.result_ready.emit()
return
self.progress.emit(os.path.basename(root))
self.result_ready.emit()
@Slot()
def stop(self):
with QMutexLocker(self.mutex):
self.interruption_requested = True
@Slot()
def reset(self):
with QMutexLocker(self.mutex):
self.interruption_requested = False
class Controller(QWidget):
operate = Signal()
def __init__(self):
super().__init__()
layout = QVBoxLayout()
self.setLayout(layout)
self.start_button = QPushButton('Start background thread')
self.start_button.clicked.connect(self.on_start_button_clicked)
self.cancel_button = QPushButton('Cancel')
self.cancel_button.clicked.connect(self.on_cancel_button_clicked)
self.cancel_button.setDisabled(True)
self.label = QLabel()
self.label.setAlignment(Qt.AlignmentFlag.AlignCenter)
layout.addWidget(self.start_button)
layout.addWidget(self.cancel_button)
layout.addWidget(self.label)
# 2. Create the thread
self.worker_thread = QThread()
# 3. Create the worker and move it to the thread
self.worker = Worker()
self.worker.moveToThread(self.worker_thread)
# 4. Connect the signals with the slots
self.worker_thread.finished.connect(self.worker.deleteLater)
self.operate.connect(self.worker.do_work)
self.worker.result_ready.connect(self.handle_results)
self.worker.progress.connect(self.label.setText)
# 5. Start the thread
self.worker_thread.start()
# 6. On the start button click emit the operate signal
@Slot()
def on_start_button_clicked(self):
self.start_button.setDisabled(True)
self.cancel_button.setEnabled(True)
self.worker.reset()
self.operate.emit()
# 7. On the cancel button click stop the worker
@Slot()
def on_cancel_button_clicked(self):
self.start_button.setEnabled(True)
self.cancel_button.setDisabled(True)
self.worker.stop()
@Slot()
def handle_results(self):
self.label.setText('Worker finished')
# 8. Quit the thread when the main window is closed
def closeEvent(self, event):
try:
self.worker.stop()
self.worker_thread.quit()
self.worker_thread.wait()
except Exception as e:
print(e)
event.accept()
if __name__ == '__main__':
app = QApplication(sys.argv)
main_window = Controller()
main_window.show()
sys.exit(app.exec())
Let’s see a more realistic example of reusing the worker thread. To walk the file system from the worker thread method
-
Create the worker class. There are several differences from the first walk filesystem example: Instead of using
QThread.isInterruptionRequested()
to check if we should interrupt thedo_work()
method we use a boolean flag namedWorker.interruption_requested
. We also add two methods,Worker.stop()
andWorker.reset()
to setinterruption_requested
toTrue
andFalse
. Finally each use ofWorker.interruption_requested
is guarded with theQMutextLocker
-QMutex
pair. -
In
Controller.__init__()
create the worker thread object, -
Create the worker object and move it to the worker thread using
QObject.moveToThread()
-
Connect the signals with the slots. When the main class emits the
operate
signal theWorker.do_work()
method is executed. -
Start the worker thread.
-
On the start button click reset the worker using
Worker.reset()
and emit theoperate
signal, -
On the cancel button click stop the worker using
Worker.stop()
, -
Quit the thread when the main window is closed.
But how did we end up using a boolean flag guarded with QMutexLock
to control the worker execution? Turns out that QThread.requestInterruption()
can only be used one time: once you use it to request the thread interruption QThread.isInterruptionRequested()
will always return True
and there is no way you can un-request it. Sending a signal from the main thread to toggle a custom boolean flag in the worker (ie Worker.interruption_requested
) would work but only if we used QApplication.processEvents()
- we have a blocking loop which means no signals are processed otherwise. Setting the flag directly might seem to work but it is not thread-safe as the worker and the main class are in different threads, so we guard each use of Worker.interruption_requested
with QMutex
- QMutexLock
making sure its value is consistent.