Stop thread that's running a while loop
-
wrote on 3 Sept 2023, 14:19 last edited by
I've tried to avoid needing multithreading as long as possible but now I need something that is running while not blocking the main window. The more I read about multithreading, the more I got scared of it ("a million ways to do it wrong if you don't absolutely know everything about all of it" seems a common comment).
So I decided to come up with a minimal example for myself to see how to do it. I've used two books and a couple of tutorials.
What I got works, but now I'd like to break a while loop running in a thread.
One thing to note: I've read multiple times that it is better to subclass your worker from
QObject
than fromQThread
, so that is what I did.Yes, this might be considered cargo cult programming. I'm trying to learn and can't understand everything right away.
import datetime import random import sys import time from PyQt5 import QtWidgets as qtw from PyQt5 import QtGui as qtg from PyQt5 import QtCore as qtc ######################### # Subclass from QObject # ######################### class WorkerClass(qtc.QObject): ############################################### # Define signals at class level, not instance # ############################################### loop_start_signal = qtc.pyqtSignal(int) loop_done_signal = qtc.pyqtSignal(str) def __init__(self): super().__init__() ################################ # Define slots using decorator # ################################ @qtc.pyqtSlot() def loop_in_worker(self): self.running: bool = True # while self.running: # nope. while not self.isInterruptionRequested: # Not a member of QObject sleep_int = random.randrange(1, 5) self.loop_start_signal.emit(sleep_int) time.sleep(sleep_int) ######################################################## # Emit signals that are linked to slots in main window # ######################################################## self.loop_done_signal.emit( datetime.datetime.now(datetime.timezone.utc).strftime('%H:%M:%S')) @qtc.pyqtSlot() def stop_looping(self): self.running = False class MainWindow(qtw.QMainWindow): start_looping = qtc.pyqtSignal() stop_thread_signal = qtc.pyqtSignal() def __init__(self): """MainWindow constructor.""" super().__init__() ################ # Main UI code # ################ layout = qtw.QFormLayout() self.go_button = qtw.QPushButton('Start Hashing') self.go_button.setCheckable(True) layout.addRow('Button:', self.go_button) self.next_sleep_length = qtw.QLabel() layout.addRow('Next sleep length:', self.next_sleep_length) main_widget = qtw.QWidget() main_widget.setLayout(layout) self.setCentralWidget(main_widget) ####################################### # Create a worker object and a thread # ####################################### self.worker = WorkerClass() self.worker_thread = qtc.QThread() ################################################ # Connect local signals to slots in the Worker # # BEFORE moving worker to thread # ################################################ # self.go_button.clicked.connect(self.worker.loop_in_worker) self.go_button.clicked.connect(self.handle_button_clicked) self.start_looping.connect(self.worker.loop_in_worker) self.stop_thread_signal.connect(self.worker.stop_looping) ############################################## # Connect signals from Worker to local slots # # BEFORE moving worker to thread # ############################################## self.worker.loop_done_signal.connect(self.receive_done_from_worker) self.worker.loop_start_signal.connect(self.receive_sleep_length_from_worker) ######################################################## # Assign the worker to the thread and start the thread # ######################################################## self.worker.moveToThread(self.worker_thread) self.show() ################################ # Define slots using decorator # ################################ @qtc.pyqtSlot(bool) def handle_button_clicked(self, button_down: bool): print(f'Button down: {button_down}') if button_down: self.worker_thread.start() self.start_looping.emit() else: ################################### # This is where I'm stuck. # # How do I stop the while loop or # # forcibly interrupt the thread? # ################################### self.worker_thread.requestInterruption( ) # My class isn't a subclass from QThread so doesn't know that # self.stop_thread_signal.emit() # doesn't get handled by the thread event loop # self.worker_thread.quit() # Doesn't seem to do anything # # self.worker_thread.wait() # keeps waiting because while loop isn't interrupted @qtc.pyqtSlot(int) def receive_sleep_length_from_worker(self, length: int): self.next_sleep_length.setText(f'{length} seconds') @qtc.pyqtSlot(str) def receive_done_from_worker(self, donetime: str): print(f'Done: {donetime}') if __name__ == '__main__': app = qtw.QApplication(sys.argv) mw = MainWindow() sys.exit(app.exec())
I've tried several things:
- using a self.running attribute but the thread is obviously stuck in the while loop so the event handler doesn't get to the slot I call.
requestInterruption()
but my worker isn't subclassed fromQThread
- A number of things I've since learned aren't a good idea.
What is the "correct" way to terminate this thread (preferably in a way so that I can start it again).
I hope my example code is clear. It's just a button that starts a thread that contains a while loop that executes
sleep()
or random lengths and "reports back" to the main window. Once again, this is not meant to do anything useful besides showing me how to work with threads. I'd like to avoid as much complication as reasonable. -
Hi,
The first question to answer is: which type of threading technique do you need ? Worker object or custom thread ? Both can be valid and thus determining which one will yield different result in how things can be stopped.
-
Hi,
The first question to answer is: which type of threading technique do you need ? Worker object or custom thread ? Both can be valid and thus determining which one will yield different result in how things can be stopped.
wrote on 3 Sept 2023, 18:14 last edited byThank you for your reply. I tried making my example code as simple as possible to make it clear (for this forum as well as for myself), so the reason for requiring threading is indeed not clear.
I have not seen the question you ask come up before in what I read so far. Maybe it is obvious for most people, but I don't know what you mean exactly I'm afraid...
The bigger picture is this: I've written a piece of software as a favor for a company that I sometimes work for. I had no decent experience with PyQt before. Obviously, what started as "we just need to do x" became a much bigger piece of software. It's been a very good learning experience. However, when the software is used (checking in guests at conferences), having it fail while there's queues of people waiting to get is not a pleasant foresight. So I err on the side of caution whenever I implement something new.
Now I need to add syncing in real time. Different "check-in stations" where hostesses help people to scan their access codes need to wirelessly report in real time to a central spot where the client can see how much people are in, who, ...
I've decided to do this using the Reticulum network stack (as it can be extended over LoRa and VHF amongst other things). So I need to write a handler that takes care of the connection, sends data when a new check-in is performed, responds when the link goes down, ...This seemed not something I wanted to do in the main event loop as it might take a while to connect.
Does that make sense?
Thanks again.
-
Thank you for your reply. I tried making my example code as simple as possible to make it clear (for this forum as well as for myself), so the reason for requiring threading is indeed not clear.
I have not seen the question you ask come up before in what I read so far. Maybe it is obvious for most people, but I don't know what you mean exactly I'm afraid...
The bigger picture is this: I've written a piece of software as a favor for a company that I sometimes work for. I had no decent experience with PyQt before. Obviously, what started as "we just need to do x" became a much bigger piece of software. It's been a very good learning experience. However, when the software is used (checking in guests at conferences), having it fail while there's queues of people waiting to get is not a pleasant foresight. So I err on the side of caution whenever I implement something new.
Now I need to add syncing in real time. Different "check-in stations" where hostesses help people to scan their access codes need to wirelessly report in real time to a central spot where the client can see how much people are in, who, ...
I've decided to do this using the Reticulum network stack (as it can be extended over LoRa and VHF amongst other things). So I need to write a handler that takes care of the connection, sends data when a new check-in is performed, responds when the link goes down, ...This seemed not something I wanted to do in the main event loop as it might take a while to connect.
Does that make sense?
Thanks again.
I would start by first getting this part running and then benchmark it.
After that, consider threading. Depending on what you need to do, using a message queue might be enough (for example if just sending updates from the client to a central server).
-
wrote on 5 Sept 2023, 06:28 last edited by
while self.running:
would at least work inside C++. Python has additional quirks due to the Global Interpreter Lock. I don't use Python and thus don't know the details.As you have figured out
isInterruptionRequested
is a method of QThread. QThread also has a class method (in C++, don't know how this translates to Python)currentThread()
to get the thread of the context of your code. So, in C++ it would look like this:while(!QThread::currentThread().isInterruptionRequested()) ...
.
1/5