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

  1. 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 the do_work() method we use a boolean flag named Worker.interruption_requested. We also add two methods, Worker.stop() and Worker.reset() to set interruption_requested to True and False. Finally each use of Worker.interruption_requested is guarded with the QMutextLocker - QMutex pair.

  2. In Controller.__init__() create the worker thread object,

  3. Create the worker object and move it to the worker thread using QObject.moveToThread()

  4. Connect the signals with the slots. When the main class emits the operate signal the Worker.do_work() method is executed.

  5. Start the worker thread.

  6. On the start button click reset the worker using Worker.reset() and emit the operate signal,

  7. On the cancel button click stop the worker using Worker.stop(),

  8. 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.