QThread doesn't finish - python

I am building a GUI with a socket connection running in a backround thread in PyQt5. Everything is working pretty well except the Qthread never emits the finished signal. Maybe this isn't a problem and I could change my implementation around to work around it, but is the expected behavior that the Qthread would continue to run once the object that has been moved has stopped doing anything?
Should I write a function in the main class to stop the thread once I'm done with it, or can I just move new things to that thread with no consequences?
class MyClass(PyQt5.QtWidgets.QMainWindow)
def __init__(self, parent=None):
# Setup thread for the socket control
self.simulThread = PyQt5.QtCore.QThread()
self.socketController = SocketController()
self.socketController.moveToThread(self.simulThread)
self.simulThread.started.connect(self.socketController.controlSocket)
self.simulThread.finished.connect(self.deadThread)
# Bind controls to events
self.ui.buttonConnect.clicked.connect(self.simulThread.start)
self.ui.buttonDisconnect.clicked.connect(lambda: self.socketController.stop())
def deadThread(self, data):
print ("THREAD IS DEAD.")
class SocketController(PyQt5.QtCore.QObject):
finished = PyQt5.QtCore.pyqtSignal()
def __init__(self):
super(SocketController, self).__init__()
self.run = True;
def controlSocket(self):
#setup the socket
while self.run:
# do socket stuff
time.sleep(1)
#close the socket
self.finished.emit()
def stop(self):
self.run = False;

A QThread has it's own event loop to handle signals. The controlSocket method blocks this event loop until self.run == False. But this never happens because self.run is only set to False when control is returned to the event lop and it can process the signal that runs the stop method.
You probably want to rearchitect your thread so that instead of having a while loop which blocks the QThread event loop, you construct a QTimer in the thread which calls the #do socket stuff code every 1 second. This way, control is returned to the threads event loop and it can process the stop signal (which would be changed to stop the QTimer from triggering again)

The answer is actually a combination of what three_pineapples and OP posted in their answers:
As noted by three_pineapples, the main problem is that controlSocket(), which gets called by the thread event loop when started signal is emitted, doesn't return until the stop() method gets called. However, in the OP's design, the stop() method can only be called by Qt if the socket thread can process events (since this is how cross-thread signals are dispatched, via the event loops). This is not possible while controlSocket() is busy looping and sleeping.
In order for the thread to exit, its event loop must be stopped.
The first issue must be fixed by either allowing the thread to process events during the loop, or by using a timer instead of a loop. Processing events is shown in this piece of code, based on the OP code:
import time
from PyQt5.QtWidgets import QMainWindow, QWidget, QPushButton, QApplication, QHBoxLayout
from PyQt5.QtCore import QThread, QObject, pyqtSignal
class MyClass(QWidget):
def __init__(self, parent=None):
super().__init__()
self.resize(250, 150)
self.setWindowTitle('Simple')
# Setup thread for the socket control
self.socketController = SocketController()
self.simulThread = QThread()
self.socketController.moveToThread(self.simulThread)
self.simulThread.started.connect(self.socketController.controlSocket)
self.simulThread.finished.connect(self.deadThread)
# Bind controls to events
self.buttonConnect = QPushButton('connect')
self.buttonConnect.clicked.connect(self.simulThread.start)
self.buttonDisconnect = QPushButton('disconnect')
self.buttonDisconnect.clicked.connect(self.socketController.stop)
hbox = QHBoxLayout()
hbox.addStretch(1)
hbox.addWidget(self.buttonConnect)
hbox.addWidget(self.buttonDisconnect)
self.setLayout(hbox)
def deadThread(self, data):
print("THREAD IS DEAD.")
class SocketController(QObject):
finished = pyqtSignal()
def __init__(self):
print('initialized')
super().__init__()
self.run = True
def controlSocket(self):
# setup the socket
print('control socket starting')
while self.run:
# do socket stuff
app.processEvents()
print('control socket iterating')
time.sleep(1)
# close the socket
self.finished.emit()
print('control socket done')
def stop(self):
print('stop pending')
self.run = False
app = QApplication([])
mw = MyClass()
mw.move(300, 300)
mw.show()
app.exec()
The QTimer is a little more complicated, but the idea is to have controlSocket() only do one iteration of the loop, and call it repeatedly via QTimer:
QTimer.singleShot(0, self.controlSocket)
and
def controlSocket(self):
# do socket stuff, then:
if self.run:
QTimer.singleShot(1000, self.controlSocket)
else:
self.finished.emit()
Either of the above approaches fixes the first problem, and allows the SocketController to stop doing its socket-related work.
The second problem is that even after the socket controller is done its job, the thread loop is still running (the two loops are independent). To make the event loop exit, it must be stopped via the thread's quit() method. The quit and the stop should be done together:
class MyClass(QWidget):
def __init__(self, parent=None):
...
self.buttonDisconnect = QPushButton('disconnect')
self.buttonDisconnect.clicked.connect(self.stop)
hbox = QHBoxLayout()
hbox.addStretch(1)
hbox.addWidget(self.buttonConnect)
hbox.addWidget(self.buttonDisconnect)
self.setLayout(hbox)
def stop(self):
self.simulThread.quit()
self.socketController.stop()

Ok, so I found A solution, but I don't really know if its correct. Instead of mapping the disconnect button to the socket stop function, I mapped it to function defined in MyClass that calls a new function I wrote called controlDisconnect which also tells the thread explicitly to quit.
# The changed line
self.ui.buttonDisconnect.clicked.connect(self.controlDisconnect)
def controlDisconnect(self):
self.socketController.stop()
self.simulThread.quit()
While this is working (the thread prints out the message that indicates it died), I'm not sure its really good practice. There should probably at least be some code to make sure the socketController has actually stopped before telling the thread to quit.

Related

Creating multiple QThreads and perform action AFTER all QThreads are closed

here I have a simple GUI. With the creation of this GUI I am starting a thread (THREAD1) which is running while the GUI is running. The task of this thread (THREAD1) is, to perform a specific action in a specific interval.
It is important, that this specific action is performed as fast as possible. So I am creating new thread objects. This is accomplished by THREAD1
Until here everything is working fine. I get the THREAD1 to work. I am also able to get threads created by THREAD1 to work.
here the code.
from PyQt6.QtCore import QThread, QObject
from PyQt6.QtWidgets import QApplication, QMainWindow, QWidget
import time
class GetMarketDataV2(QThread):
def __init__(self, index):
super(GetMarketDataV2, self).__init__()
self.index = index
def run(self):
time.sleep(5)
print(f"Thread: {self.index}\n")
class DataCollectionLoop(QObject):
"""
Runs as long as the GUI is running. Terminated with the Main window close event.
The task of this class is, to start a specific amount of threads with a specific interval.
"""
def __init__(self):
super(DataCollectionLoop, self).__init__()
self.thread = {}
self.first_startup = True
def run(self):
while True:
# In this example 10 threads are created.
for i in range(10):
self.thread[i] = GetMarketDataV2(index=i)
self.thread[i].start()
# I want this line below to execute after all threads above are done with their job.
print("print this statement after all threads are finished")
# Here the specific interval is 10 seconds.
time.sleep(10)
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
# Setting up thread which controls the data collection.
self.thread = QThread()
# Instantiating the DataCollectionLoop object and moving it to a thread.
data_collection = DataCollectionLoop()
data_collection.moveToThread(self.thread)
# calling the run method when the thread is started.
self.thread.started.connect(data_collection.run)
# Starting the thread
self.thread.start()
# Minimal GUI
self.centralWidget = QWidget()
self.setCentralWidget(self.centralWidget)
self.show()
# Terminating the thread when window is closed.
def closeEvent(self, event):
self.thread.terminate()
app = QApplication([])
window = MainWindow()
I have searched a decent amount of time, but couldn't find a solution to my problem.
Which is:
I want to wait for all threads, which are created by THREAD1, to finish, before continuing with my code.
I think I should catch the states of each thread (if they are finished or not) but I don't know exactly how..
Thank you very much!
As #musicamante suggested I got the desired result using the isFinished() method.
Here is how I made it:
# Waits for all threads to finnish. Breaks out of the loop afterwards
while True:
for thread in self.thread.values():
if not thread.isFinished():
break
else:
break
Another solution I stumpled uppon which does the trick for me, is to work with QRunnables and a Threadpool.
for market in forex_markets:
market = GetMarketData() # Subclassed QRunnable
self.threadpool.start(market)
self.threadpool.waitForDone()
Thank you!

Qthread communication between threads (Python) [duplicate]

After read and searching I am trying to use the generate a QObject then use the movetoThread method to run an independent process and allow the QMainWindow to continue to respond. This has not worked when I have tried to implement the operation in a QThread.run() method. The following code is my attempt to make a simple example. While the code works in running thread independent of the MainWindow, it does not abort. The only way I can get a thread to stop is to set worker.end = True. Which I think should not be the way to do it.
"""
This is a program to test Threading with Objects in PyQt4.
"""
from time import sleep
import sys
from PyQt4.QtCore import QObject, pyqtSlot, pyqtSignal, QThread
from PyQt4.QtGui import QMainWindow, QApplication, QProgressBar
from PyQt4.QtGui import QPushButton, QVBoxLayout, QWidget
class workerObject(QObject):
bar_signal = pyqtSignal(int)
res_signal = pyqtSignal(str)
term_signal = pyqtSignal()
def __init__(self, maxIters):
super(workerObject, self).__init__()
self.maxIters = maxIters
def run(self):
self.bar_signal.emit(self.maxIters)
sleep(1)
self.end = False
for step in range(self.maxIters):
if self.end:
self.maxIters = step
break
self.bar_signal.emit(step)
sleep(2)
self.res_signal.emit("Got to {}".format(self.maxIters))
self.term_signal.emit()
#pyqtSlot()
def mystop(self):
print "stop signalled?"
self.end = True
class MCwindow(QMainWindow):
abort_signal = pyqtSignal(name='abort_signal')
def __init__(self):
super(MCwindow,self).__init__()
self.maxIters = 50
widget = QWidget()
layout = QVBoxLayout(widget)
self.go_btn = QPushButton()
self.go_btn.setText('Go')
layout.addWidget(self.go_btn)
self.abort_btn = QPushButton()
self.abort_btn.setText('Stop')
layout.addWidget(self.abort_btn)
self.simulation_bar = QProgressBar()
self.simulation_bar.setRange(0, self.maxIters)
self.simulation_bar.setFormat("%v")
layout.addWidget(self.simulation_bar)
self.setCentralWidget(widget)
self.go_btn.clicked.connect(self.run_mc)
# The button calls the windows method to stop --- it could
# be that is 'clicked' calls the worker.mystop
# self.abort_btn.clicked.connect(self.stop_mc)
# This allows for the abort button to do somethign in the MainWindow
# before the abort_signal is sent, this works
self.abort_btn.clicked.connect(self.stop_mc)
def run_mc(self):
self.thread = QThread()
self.worker = workerObject(self.maxIters)
self.worker.moveToThread(self.thread)
self.thread.started.connect(self.worker.run)
# This is the simple stop method, but does not work
# self.abort_btn.clicked.connect(self.worker.mystop)
# This uses the signal in the MCwindow - this connection does NOT works
self.abort_signal.connect(self.worker.mystop)
# This does NOT stop the thread
# and would not allow for any clean up in the worker.
# self.abort_signal.connect(self.thread.terminate)
# This is a 'bad' way to stop the woker ... It does, however, work
# self.abort_signal.connect(self.stopper)
self.worker.bar_signal.connect(self.setBar)
self.worker.res_signal.connect(self.setData)
self.worker.term_signal.connect(self.thread.terminate)
self.thread.start()
def stop_mc(self):
print "Stopping?!"
# This signal is NEVER seen by the Worker.
self.abort_signal.emit()
def stopper(self):
print "I should stop?!"
# Should use signals to tell the worker to stop - and not setting a attribute
self.worker.end=True
#pyqtSlot(int)
def setBar(self, val):
self.simulation_bar.setValue(val)
#pyqtSlot(str)
def setData(self, txt):
print "Got done Sig!", txt
if __name__ == '__main__':
app = QApplication(sys.argv)
window = MCwindow()
window.show()
sys.exit(app.exec_())
The reason why the slot connected to abort_signal doesn't seem to get called, is because cross-thread signals are queued by default. This means the signal will be wrapped as an event and posted to the event queue of whichever thread the receiver is living in.
In your particular example, the receiver is a worker object which has been moved to a worker thread. Calling start() on the worker thread will start its event-loop, and that is where abort_signal will be queued. However, the run() method of the worker object starts a for loop, which will block the thread's event processing in exactly the same way it would if it was executed in the main gui thread!
You can more clearly see what's happening if you make a few adjustments to your example:
class MCwindow(QMainWindow):
abort_signal = pyqtSignal(name='abort_signal')
def __init__(self):
super(MCwindow,self).__init__()
# use a sane default
self.maxIters = 5
...
# DO NOT use QThread.terminate
self.worker.term_signal.connect(self.thread.quit)
Now run the example, and then click the Go button, click the Stop button, and wait for the worker to complete normally. This should produce output like this:
Stopping?!
Got done Sig! Got to 5
stop signalled?
Note that "stop signalled" is output last - i.e. after run() exits and control has returned to the thread's event-loop. In order to process in-coming signals while the worker is running, you will need to force immediate processing of the thread's pending events. This can be done like this:
for step in range(self.maxIters):
QApplication.processEvents()
...
With that in place, you should then see output like this:
Stopping?!
stop signalled?
Got done Sig! Got to 2
Which is presumably what you intended.
Typically a thread will close when it exits the run method. The other way to get a regular python thread to close is by calling it's join method.
For PyQt the join method should either be the quit or terminate method. You should probably still set your end variable to True.

PyQt5 black window while threads are working

I am making window application in PyQt5 and I want to parse data from XML file in the backgrond and send it to second class. I want to use threads with queue to handle between them. When I want to display window app i see black window. It would be nice to use python threads but i tried to do it on QThread and it is not working too idk why...
This is code example
import queue
import sys
import threading
from PyQt5.QtCore import QThread, pyqtSignal, pyqtSlot
from PyQt5.QtWidgets import *
class Test_generator:
def __init__(self, queue_obj):
super().__init__()
self.queue_obj = queue_obj
#self.parser_thread = threading.Thread(target=self.parser())
def sender(self):
for _ in range(100):
string ="test"
self.queue_obj.put(string)# send to queue
time.sleep(1)
#print(string)
class Test_collectioner:
def __init__(self,queue_obj):
self.queue_obj = queue_obj
def get_data(self):
collection = []
while self.queue_obj.empty() is False:
print("xd")
print(self.queue_obj.get(), "xd")
#I found this example in the internet(Not working)
class Threaded(QThread):
result = pyqtSignal(int)
def __init__(self, parent=None, **kwargs):
super().__init__(parent, **kwargs)
#pyqtSlot(int)
def run(self):
while True:
print("test")
class MainWindow(QMainWindow):
def __init__(self):
super(MainWindow, self).__init__()
self.GUI()
self.setWindowTitle("PyQt5 app")
global q
q = queue.Queue()
# BLACKSCREEN (for Test_generator)
test_generator_obj = Test_generator(q)
test_collectioner_obj = Test_collectioner(q)
t1 = threading.Thread(target=test_generator_obj.sender())
t2 = threading.Thread(target=test_collectioner_obj.get_data())
t1.start()
t2.start()
# BLACKSCREEN TOO
"""self.thread = QThread()
self.threaded = Threaded()
self.thread.started.connect(self.threaded.run())
self.threaded.moveToThread(self.thread)
qApp.aboutToQuit.connect(self.thread.quit)
self.thread.start()"""
def GUI(self):
self.showMaximized()
if __name__ == '__main__':
app = QApplication(sys.argv)
window = MainWindow()
sys.exit(app.exec_())
There are two main problems in your code:
you're executing the methods used for the thread, but you should use the callable object instead; you did the same mistake for the QThread, since connections to signals also expects a callable, but you're actually executing the run function from the main thread (completely blocking everything), since you're using the parentheses; those lines should be like this:
t1 = threading.Thread(target=test_generator_obj.sender)
t2 = threading.Thread(target=test_collectioner_obj.get_data)
or, for the QThread:
self.thread.started.connect(self.threaded.run)
due to the python GIL, multithreading only releases control to the other threads whenever it allows to, but the while cycle you're using prevents that; adding a small sleep function ensures that control is periodically returned to the main thread;
Other problems also exist:
you're already using a subclass of QThread, so there's no use in using another QThread and move your subclass to it;
even assuming that an object is moved to another thread, the slot should not be decorated with arguments, since the started signal of a QThread doesn't have any; also consider that slot decorators are rarely required;
the thread quit() only stops the event loop of the thread, but if run is overridden no event loop is actually started; if you want to stop a thread with a run implementation, a running flag should be used instead; in any other situation, quitting the application is normally enough to stop everything;
Note that if you want to interact with the UI, you can only use QThread and custom signals, and basic python threading doesn't provide a suitable mechanism.
class Threaded(QThread):
result = pyqtSignal(int)
def __init__(self, parent=None, **kwargs):
super().__init__(parent, **kwargs)
def run(self):
self.keepGoing = True
while self.keepGoing:
print("test")
self.msleep(1)
def stop(self):
self.keepGoing = False
class MainWindow(QMainWindow):
def __init__(self):
# ...
self.threaded = Threaded()
self.threaded.start()
qApp.aboutToQuit.connect(self.threaded.stop)

Why is processEvents() needed to get QThread to work?

Below is my code for listing all the sub-directories of a directory. I'm using it to understand QThread and signal and slots in PySide. The problem is, when I'm not using Qtcore.QApplication.processEvents() in the scan() method of the Main class, the code does not work. Is the event-loop not already running?
import sys
import os
import time
from PySide import QtGui, QtCore
class Scanner(QtCore.QObject):
folderFound = QtCore.Signal(str)
done = QtCore.Signal()
def __init__(self, path):
super(Scanner, self).__init__()
self.path = path
#QtCore.Slot()
def scan(self):
for folder in os.listdir(self.path):
time.sleep(1)
self.folderFound.emit(folder)
class Main(QtGui.QWidget):
def __init__(self):
QtGui.QWidget.__init__(self)
self.resize(420,130)
self.setWindowTitle('Threads')
self.lyt = QtGui.QVBoxLayout()
self.setLayout(self.lyt)
self.topLyt = QtGui.QHBoxLayout()
self.lyt.addLayout(self.topLyt)
self.scanBtn = QtGui.QPushButton('Scan')
self.scanBtn.clicked.connect(self.scan)
self.clearBtn = QtGui.QPushButton('Clear')
self.topLyt.addWidget(self.scanBtn)
self.topLyt.addWidget(self.clearBtn)
self.folders = list()
self.show()
def scan(self):
self.th = QtCore.QThread()
scanner = Scanner(r"D:\\")
scanner.moveToThread(self.th)
scanner.folderFound.connect(self.addFolder)
scanner.done.connect(scanner.deleteLater)
scanner.done.connect(self.quit)
self.th.started.connect(scanner.scan)
self.th.start()
QtCore.QApplication.processEvents()
#QtCore.Slot()
def addFolder(self, folder):
lbl = QtGui.QLabel(folder)
self.folders.append(lbl)
self.lyt.addWidget(lbl)
#QtCore.Slot()
def quit(self):
self.th.quit()
self.th.wait()
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
main = Main()
app.exec_()
It is a pure fluke that your example works at all.
When you attempt to call QtCore.QApplication.processEvents(), a NameError will be raised, because the QApplication class is actually in the QtGui module, not the QtCore module. However, raising the exception has the side-effect of preventing your scanner object from being garbage-collected, and so the thread appears to run normally.
The correct way to fix your code is to keep a reference to the scanner object, and get rid of the processEvents line, which is not needed:
def scan(self):
self.th = QtCore.QThread()
# keep a reference to the scanner
self.scanner = Scanner(r"D:\\")
self.scanner.moveToThread(self.th)
self.scanner.folderFound.connect(self.addFolder)
self.scanner.done.connect(self.scanner.deleteLater)
self.scanner.done.connect(self.quit)
self.th.started.connect(self.scanner.scan)
self.th.start()
The rest of your code is okay, and the example will now work as expected.
Event loop is not something that runs behind your back. You're always the one who has to run it. A thread cannot be doing two things at once: if your code is the locus of control, then obviously the event loop isn't! You need to return from your code to the event loop, and make sure that your code was called from the event loop. Any sort of a UI signal, networking event, or timeout is invoked from the event loop, so most likely your code already has the event loop on the call stack. To keep the loop spinning, you have to return to it, though.
Never use processEvents - instead, invert the control, so that the event loop calls into your code, and then you perform a chunk of work, and finally return back to the event loop.
The idiom for "keep my code working from event loop" is a zero-duration timer. The callable that performs the work is attached to the timeout signal.

Updating QtGui elements from python

I wrote a small application using QT and Python.
I Press a button, a wait for serial input for 5 seconds. I have few labels
which I want to update e.g., when I press button it should change to 'starting reading' and when I return it should change to 'reading done'. I use
a simple thread which calls processEvents but it does not gets updated and when read function finishes I see the last label change.
class MyWindow(QtGui.QMainWindow):
def __init__(self):
print 'myWindow'
super(MyWindow, self).__init__()
uic.loadUi('test1.ui', self)
QtCore.QObject.connect(self.pushButton, QtCore.SIGNAL ('clicked()'), self.buttonStartClicked)
self.show()
def buttonStartClicked(self):
thread = threading.Thread(target = self.update_gui, args = ())
thread.daemon = True
thread.start()
self.label.setText('Starting Test')
response = sRS232_Con.read()
#QtGui.QApplication.processEvents()
self.label.setText('Ending Test')
def update_gui(self):
while True :
QtGui.QApplication.processEvents()
print 'update'
time.sleep(1)
def main():
app = QtGui.QApplication(sys.argv)
window = MyWindow()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
In Qt any QObject that is managed by some event loop should not be touch by other thread (at least without any protection).
If for some reason you cannot use QSerialPort library that provides asynchronous API for serial port it is possible to use QThread for separate thread managed by Qt signals and slots.
Events for QtGui are dispatched by the main thread (insided of the loop app.exec_()).
Another QThread can work with serial port and it can emit signals on 'Starting Test' and on 'Ending Test'. Those signals can be connected to MyWindow slots that are able to update UI in the main thread.

Categories