PyQt4:GUI关闭时中断QThread执行
我有一个具有三个线程的 PyQt4 GUI。一个线程是一个数据源,它提供 numpy 数据数组。下一个线程是计算线程,它通过 Python Queue.Queue 获取 numpy 数组(或多个 numpy 数组)并计算将在 GUI 上显示的内容。然后计算器通过自定义信号向 GUI 线程(主线程)发出信号,这告诉 GUI 更新显示的 matplotlib 图形。
我正在使用此处描述的“正确”方法 和 这里。
这是总体布局。我试图缩短我的打字时间,并在某些部分使用注释而不是实际代码:
class Source(QtCore.QObject):
signal_finished = pyQtSignal(...)
def __init__(self, window):
self._exiting = False
self._window = window
def do_stuff(self):
# Start complicated data generator
for data in generator:
if not self._exiting:
# Get data from generator
# Do stuff - add data to Queue
# Loop ends when generator ends
else:
break
# Close complicated data generator
def prepare_exit(self):
self._exiting = True
class Calculator(QtCore.QObject):
signal_finished = pyQtSignal(...)
def __init__(self, window):
self._exiting = False
self._window = window
def do_stuff(self):
while not self._exiting:
# Get stuff from Queue (with timeout)
# Calculate stuff
# Emit signal to GUI
self._window.signal_for_updating.emit(...)
def prepare_exit(self):
self._exiting = True
class GUI(QtCore.QMainWindow):
signal_for_updating = pyQtSignal(...)
signal_closing = pyQtSignal(...)
def __init__(self):
self.signal_for_updating.connect(self.update_handler, type=QtCore.Qt.BlockingQueuedConnection)
# Other normal GUI stuff
def update_handler(self, ...):
# Update GUI
def closeEvent(self, ce):
self.fileQuit()
def fileQuit(self): # Used by a menu I have File->Quit
self.signal_closing.emit() # Is there a builtin signal for this
if __name__ == '__main__':
app = QtCore.QApplication([])
gui = GUI()
gui.show()
source_thread = QtCore.QThread() # This assumes that run() defaults to calling exec_()
source = Source(window)
source.moveToThread(source_thread)
calc_thread = QtCore.QThread()
calc = Calculator(window)
calc.moveToThread(calc_thread)
gui.signal_closing.connect(source.prepare_exit)
gui.signal_closing.connect(calc.prepare_exit)
source_thread.started.connect(source.do_stuff)
calc_thread.started.connect(calc.do_stuff)
source.signal_finished.connect(source_thread.quit)
calc.signal_finished.connect(calc_thread.quit)
source_thread.start()
calc_thread.start()
app.exec_()
source_thread.wait() # Should I do this?
calc_thread.wait() # Should I do this?
...所以,当我尝试在源完成之前关闭 GUI 时,当我让数据生成器完成它关闭时,我的问题都会发生fine:
在等待线程时,程序挂起。据我所知,这是因为关闭信号的连接槽永远不会被其他线程的事件循环运行(它们卡在“无限”运行的 do_stuff 方法上)。
当 calc 线程在 GUI 关闭后立即发出更新 gui 信号(BlockedQueuedConnection 信号)时,它似乎挂起。我猜测这是因为 GUI 已经关闭并且无法接受发出的信号(根据我在实际代码中放入的打印消息判断)。
我一直在浏览大量的教程和文档,但我只是觉得我在做一些愚蠢的事情。这是否可能有一个事件循环和一个提前结束的“无限”运行循环......并且安全(资源正确关闭)?
我也对我的 BlockedQueuedConnection 问题感到好奇(如果我的描述有意义的话),但是这个问题可能可以通过我没有看到的简单重新设计来解决。
感谢您的帮助,让我知道什么是没有意义的。如果需要的话,我还可以在代码中添加更多内容,而不仅仅是做注释(我有点希望我做了一些愚蠢的事情,但不需要)。
编辑:我找到了一些解决办法,但是,我认为我很幸运,到目前为止它每次都有效。如果我进行prepare_exit和thread.quit连接DirectConnections,它会在主线程中运行函数调用,并且程序不会挂起。
我还认为我应该总结一些问题:
- QThread 可以有一个事件循环(通过 exec_)并且有一个长时间运行的循环吗?
- 如果接收器断开插槽连接(之后),BlockingQueuedConnection 发射器是否会挂起 ?信号已发出,但在确认之前)?
- 我应该在 app.exec_() 之后等待 QThreads(通过 thread.wait())吗?
- >有没有一个Qt 提供了 QMainWindow 何时关闭的信号,或者 QApplication 是否有信号?
编辑 2/更新进度: 我通过调整 这篇文章满足我的需求。
from PyQt4 import QtCore
import time
import sys
class intObject(QtCore.QObject):
finished = QtCore.pyqtSignal()
interrupt_signal = QtCore.pyqtSignal()
def __init__(self):
QtCore.QObject.__init__(self)
print "__init__ of interrupt Thread: %d" % QtCore.QThread.currentThreadId()
QtCore.QTimer.singleShot(4000, self.send_interrupt)
def send_interrupt(self):
print "send_interrupt Thread: %d" % QtCore.QThread.currentThreadId()
self.interrupt_signal.emit()
self.finished.emit()
class SomeObject(QtCore.QObject):
finished = QtCore.pyqtSignal()
def __init__(self):
QtCore.QObject.__init__(self)
print "__init__ of obj Thread: %d" % QtCore.QThread.currentThreadId()
self._exiting = False
def interrupt(self):
print "Running interrupt"
print "interrupt Thread: %d" % QtCore.QThread.currentThreadId()
self._exiting = True
def longRunning(self):
print "longRunning Thread: %d" % QtCore.QThread.currentThreadId()
print "Running longRunning"
count = 0
while count < 5 and not self._exiting:
time.sleep(2)
print "Increasing"
count += 1
if self._exiting:
print "The interrupt ran before longRunning was done"
self.finished.emit()
class MyThread(QtCore.QThread):
def run(self):
self.exec_()
def usingMoveToThread():
app = QtCore.QCoreApplication([])
print "Main Thread: %d" % QtCore.QThread.currentThreadId()
# Simulates user closing the QMainWindow
intobjThread = MyThread()
intobj = intObject()
intobj.moveToThread(intobjThread)
# Simulates a data source thread
objThread = MyThread()
obj = SomeObject()
obj.moveToThread(objThread)
obj.finished.connect(objThread.quit)
intobj.finished.connect(intobjThread.quit)
objThread.started.connect(obj.longRunning)
objThread.finished.connect(app.exit)
#intobj.interrupt_signal.connect(obj.interrupt, type=QtCore.Qt.DirectConnection)
intobj.interrupt_signal.connect(obj.interrupt, type=QtCore.Qt.QueuedConnection)
objThread.start()
intobjThread.start()
sys.exit(app.exec_())
if __name__ == "__main__":
usingMoveToThread()
您可以通过运行此代码并在中断信号上的两种连接类型之间交换来看到直接连接有效,因为它在单独的线程中运行,正确还是错误的做法?我觉得这是不好的做法,因为我我正在快速更改另一个线程正在阅读的内容。 QueuedConnection 不起作用,因为事件循环必须等到 longRunning 完成才能返回到中断信号,这不是我想要的。
编辑3:我记得读过QtCore.QCoreApplication.processEvents
可以在长时间运行计算的情况下使用,但我读到的所有内容都说不要使用它,除非你知道什么你正在做。好吧,这就是我认为它正在做的事情(在某种意义上)并且使用它似乎有效:当您调用 processEvents 时,它会导致调用者的事件循环停止其当前操作并继续处理事件循环中的挂起事件,最终继续长计算事件。 此电子邮件中的其他建议建议计时器或将工作投入在其他线程中,我认为这只会让我的工作变得更加复杂,特别是因为我已经证明(我认为)计时器在我的情况下不起作用。如果 processEvents 似乎解决了我所有的问题,我稍后会回答我自己的问题。
I have a PyQt4 GUI that has three threads. One thread is a data source, it provides numpy arrays of data. The next thread is a calculation thread, it takes the numpy array (or multiple numpy arrays) via a Python Queue.Queue
and calculates what will be displayed on the GUI. The calculator then signals the GUI thread (the main thread) via a custom signal and this tells the GUI to update the matplotlib figure that's displayed.
I'm using the "proper" method described here and here.
So here's the general layout. I tried to shorten my typing time and used comments instead of the actual code in some parts:
class Source(QtCore.QObject):
signal_finished = pyQtSignal(...)
def __init__(self, window):
self._exiting = False
self._window = window
def do_stuff(self):
# Start complicated data generator
for data in generator:
if not self._exiting:
# Get data from generator
# Do stuff - add data to Queue
# Loop ends when generator ends
else:
break
# Close complicated data generator
def prepare_exit(self):
self._exiting = True
class Calculator(QtCore.QObject):
signal_finished = pyQtSignal(...)
def __init__(self, window):
self._exiting = False
self._window = window
def do_stuff(self):
while not self._exiting:
# Get stuff from Queue (with timeout)
# Calculate stuff
# Emit signal to GUI
self._window.signal_for_updating.emit(...)
def prepare_exit(self):
self._exiting = True
class GUI(QtCore.QMainWindow):
signal_for_updating = pyQtSignal(...)
signal_closing = pyQtSignal(...)
def __init__(self):
self.signal_for_updating.connect(self.update_handler, type=QtCore.Qt.BlockingQueuedConnection)
# Other normal GUI stuff
def update_handler(self, ...):
# Update GUI
def closeEvent(self, ce):
self.fileQuit()
def fileQuit(self): # Used by a menu I have File->Quit
self.signal_closing.emit() # Is there a builtin signal for this
if __name__ == '__main__':
app = QtCore.QApplication([])
gui = GUI()
gui.show()
source_thread = QtCore.QThread() # This assumes that run() defaults to calling exec_()
source = Source(window)
source.moveToThread(source_thread)
calc_thread = QtCore.QThread()
calc = Calculator(window)
calc.moveToThread(calc_thread)
gui.signal_closing.connect(source.prepare_exit)
gui.signal_closing.connect(calc.prepare_exit)
source_thread.started.connect(source.do_stuff)
calc_thread.started.connect(calc.do_stuff)
source.signal_finished.connect(source_thread.quit)
calc.signal_finished.connect(calc_thread.quit)
source_thread.start()
calc_thread.start()
app.exec_()
source_thread.wait() # Should I do this?
calc_thread.wait() # Should I do this?
...So, my problems all occur when I try to close the GUI before the sources are complete, when I let the data generators finish it closes fine:
While waiting for the threads, the program hangs. As far as I can tell this is because the closing signal's connected slots never get run by the other thread's event loops (they're stuck on the "infinitely" running do_stuff method).
When the calc thread emits the updating gui signal (a BlockedQueuedConnection signal) right after the GUI closing, it seems to hang. I'm guessing this is because the GUI is already closed and isn't there to accept the emitted signal (judging by the print messages I put in my actual code).
I've been looking through tons of tutorials and documentation and I just feel like I'm doing something stupid. Is this possible, to have an event loop and an "infinite" running loop that end early...and safely (resources closed properly)?
I'm also curious about my BlockedQueuedConnection problem (if my description makes sense), however this problem is probably fixable with a simple redesign that I'm not seeing.
Thanks for any help, let me know what doesn't make sense. If it's needed I can also add more to the code instead of just doing comments (I was kind of hoping that I did something dumb and it wouldn't be needed).
Edit: I found some what of a work around, however, I think I'm just lucky that it works every time so far. If I make the prepare_exit and the thread.quit connections DirectConnections, it runs the function calls in the main thread and the program does not hang.
I also figured I should summarize some questions:
- Can a QThread have an event loop (via exec_) and have a long running loop?
- Does a BlockingQueuedConnection emitter hang if the receiver disconnects the slot (after the signal was emitted, but before it was acknowledged)?
- Should I wait for the QThreads (via thread.wait()) after app.exec_(), is this needed?
- Is there a Qt provided signal for when QMainWindow closes, or is there one from the QApplication?
Edit 2/Update on progress: I have created a runnable example of the problem by adapting this post to my needs.
from PyQt4 import QtCore
import time
import sys
class intObject(QtCore.QObject):
finished = QtCore.pyqtSignal()
interrupt_signal = QtCore.pyqtSignal()
def __init__(self):
QtCore.QObject.__init__(self)
print "__init__ of interrupt Thread: %d" % QtCore.QThread.currentThreadId()
QtCore.QTimer.singleShot(4000, self.send_interrupt)
def send_interrupt(self):
print "send_interrupt Thread: %d" % QtCore.QThread.currentThreadId()
self.interrupt_signal.emit()
self.finished.emit()
class SomeObject(QtCore.QObject):
finished = QtCore.pyqtSignal()
def __init__(self):
QtCore.QObject.__init__(self)
print "__init__ of obj Thread: %d" % QtCore.QThread.currentThreadId()
self._exiting = False
def interrupt(self):
print "Running interrupt"
print "interrupt Thread: %d" % QtCore.QThread.currentThreadId()
self._exiting = True
def longRunning(self):
print "longRunning Thread: %d" % QtCore.QThread.currentThreadId()
print "Running longRunning"
count = 0
while count < 5 and not self._exiting:
time.sleep(2)
print "Increasing"
count += 1
if self._exiting:
print "The interrupt ran before longRunning was done"
self.finished.emit()
class MyThread(QtCore.QThread):
def run(self):
self.exec_()
def usingMoveToThread():
app = QtCore.QCoreApplication([])
print "Main Thread: %d" % QtCore.QThread.currentThreadId()
# Simulates user closing the QMainWindow
intobjThread = MyThread()
intobj = intObject()
intobj.moveToThread(intobjThread)
# Simulates a data source thread
objThread = MyThread()
obj = SomeObject()
obj.moveToThread(objThread)
obj.finished.connect(objThread.quit)
intobj.finished.connect(intobjThread.quit)
objThread.started.connect(obj.longRunning)
objThread.finished.connect(app.exit)
#intobj.interrupt_signal.connect(obj.interrupt, type=QtCore.Qt.DirectConnection)
intobj.interrupt_signal.connect(obj.interrupt, type=QtCore.Qt.QueuedConnection)
objThread.start()
intobjThread.start()
sys.exit(app.exec_())
if __name__ == "__main__":
usingMoveToThread()
You can see by running this code and swapping between the two connection types on interrupt_signal that the direct connection works because its running in a separate thread, proper or bad practice? I feel like that is bad practice because I am quickly changing something that another thread is reading. The QueuedConnection does not work because the event loop must wait until longRunning is finished before the event loop gets back around to the interrupt signal, which is not what I want.
Edit 3: I remembered reading that QtCore.QCoreApplication.processEvents
can be used in cases with long running calculations, but everything I read said don't use it unless you know what you are doing. Well here is what I think it's doing (in a sense) and using it seems to work: When you call processEvents it causes the caller's event loop to halt its current operation and continue on processing the pending events in the event loop, eventually continuing the long calculation event. Other recommendations like in this email suggest timers or putting the work in other threads, I think this just makes my job even more complicated, especially since I've proven(I think) timers don't work in my case. If processEvents seems to fix all my problems I will answer my own question later.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(4)
老实说,我没有阅读所有代码。我建议不要在代码中使用循环,而是一次运行每个逻辑块。信号/槽也可以用作这些事情的透明队列。
我编写的一些生产者/消费者示例代码
https://github.com/epage/PythonUtils/blob/master/qt_ Producer_consumer.py
我编写的一些具有更高级实用程序的不同线程代码
https://github.com/epage/PythonUtils/blob/master/qt_error_display.py 是的
,我使用了循环,主要是为了示例目的,但有时你无法避免它们(比如从管道中读取)。您可以使用超时为 0 的 QTimer,或者使用一个标志来标记事物应该退出并使用互斥锁来保护它。
重新编辑1:
1. 不要将 exec_ 与长时间运行的循环混合在一起
3. PySide 要求您在退出线程后等待。
4.我不记得有这样一个,您可以将其设置为“关闭时销毁”,然后监视关闭,或者您可以从 QMainWindow 继承,覆盖 closeEvent 并触发一个信号(就像我在 qt_error_display.py 示例中所做的那样)
重新编辑2:
我建议使用默认连接类型。
重新编辑 3:不要使用 processEvents。
I honestly did not read all of the code. I would recommend against having loops in your code but instead run each logical chunk at a time. Signals/Slots can work as transparent queues for these things too.
Some producer/consumer example code I've written
https://github.com/epage/PythonUtils/blob/master/qt_producer_consumer.py
Some different threading code with more advanced utils I've written
https://github.com/epage/PythonUtils/blob/master/qt_error_display.py
Yes I used loops, mostly for example purposes but sometimes you can't avoid them (like reading from an pipe). You can either use QTimer with a timeout of 0 or have a flag to mark that things should quit and protect it with a mutex.
RE EDIT 1:
1. Don't mix exec_ with long running loops
3. PySide requires that you wait after quitting a thread.
4. I don't remember there being one, you can set it to Destroy On Close and then monitor for close or you can inherit from QMainWindow, override closeEvent and fire a signal (like I do in the qt_error_display.py example)
RE EDIT 2:
I'd recommend using the default connection types.
RE EDIT 3: Don't use processEvents.
在浏览了邮件列表档案、谷歌搜索、堆栈溢出搜索并思考我的问题到底是什么以及问题的目的是什么之后,我得出了这个答案:
简短的答案是使用 processEvents()。长的答案是,我所有的搜索结果都是人们说“使用 processEvents() 时要非常小心”和“不惜一切代价避免它”。我认为如果你正在使用它,应该避免它,因为你没有足够快地在 GUI 主线程中看到结果。在这种情况下,不应使用 processEvents,而是应将主线程中非 UI 目的的工作移至另一个线程(正如我的设计所做的那样)。
我的具体问题需要 processEvents() 的原因是我希望我的 QThreads 与 GUI 线程有两种通信方式,这意味着我的 QThreads 必须有一个事件循环 (exec_()) 来接受来自 GUI 的信号。这种双向沟通就是我之前所说的“问题的目的”。由于我的 QThreads 旨在与主 GUI 线程“同时”运行,并且因为它们需要更新 GUI 并由 GUI“更新”(我的第一个示例中的退出/关闭信号),所以它们需要 processEvents()。我认为这就是 processEvents() 的用途。
如上所述,我对 processEvents() 的理解是,当在 QThread 中调用时,它将阻止/暂停当前事件(我的 longRunning 方法),同时继续处理事件循环中的事件(仅适用于 QThread processEvents()被叫进来了)。在处理完挂起的事件之后,事件循环返回并继续运行它暂停的事件(我的 longRunning 方法)。
我知道我没有回答所有问题,但主要问题已经回答了。
如果我有任何错误,请纠正我
编辑:请阅读埃德的回答和评论。
After looking through the mailing list archives, google searching, stack overflow searching, and thinking about what my question really was and what the purpose of the question was I came up with this answer:
The short answer being use processEvents(). The long answer is that all my searching results in people saying "be very careful using processEvents()" and "avoid it at all costs". I think it should be avoided if you are using it because you are not seeing results in your GUI main thread fast enough. Instead of using processEvents in this case, the work being done in the main thread that is not UI purposed should be moved to another thread (as my design has done).
The reason my specific problem needs processEvents() is that I want my QThreads to have two way communication with the GUI thread, which means that my QThreads have to have an event loop (exec_()) to accept signals from the GUI. This two way communication is what I meant earlier by "the purpose of the question". Since my QThreads are meant to run "concurrently" with the main GUI thread AND because they need to update the GUI and be "updated" by the GUI (the exit/closing signal in my first example), they need processEvents(). I think this is what processEvents() is for.
My understanding of processEvents(), as decribed above, is that when called in a QThread it will block/pause the current event (my longRunning method) while it continues on through the events in the event loop (only for the QThread processEvents() was called in). After going through the pending events, the event loop wraps back around and continues running the event that it paused (my longRunning method).
I know I didn't answer all my questions, but the main one is answered.
PLEASE CORRECT ME IF I AM WRONG IN ANY WAY
Edit: Please read Ed's answer and the comments.
除了
QMetaObject.invokeMethod
之外,还可以使用超时为 0 的QTimer
,如下所示:https://doc.qt.io/qt-5/qtimer.htmlInstead of
QMetaObject.invokeMethod
it is also possible to useQTimer
with 0 timeout as suggested here: https://doc.qt.io/qt-5/qtimer.html您可以将工作负载分成多个块,并在单独的槽调用中一一处理它们,如下所示: https://wiki .qt.io/Threads_Events_QObjects
结果:
You could split your workload into chunks and process them one by one in separate slot calls as suggested here: https://wiki.qt.io/Threads_Events_QObjects
Result: