PySide/PyQt - 启动 CPU 密集型线程会挂起整个应用程序
Posted
技术标签:
【中文标题】PySide/PyQt - 启动 CPU 密集型线程会挂起整个应用程序【英文标题】:PySide/PyQt - Starting a CPU intensive thread hangs the whole application 【发布时间】:2012-07-01 05:20:49 【问题描述】:我正在尝试在我的 PySide GUI 应用程序中做一件相当常见的事情:我想将一些 CPU 密集型任务委托给后台线程,以便我的 GUI 保持响应,甚至可以在计算进行时显示进度指示器。
这就是我正在做的事情(我在 Python 2.7、Linux x86_64 上使用 PySide 1.1.1):
import sys
import time
from PySide.QtGui import QMainWindow, QPushButton, QApplication, QWidget
from PySide.QtCore import QThread, QObject, Signal, Slot
class Worker(QObject):
done_signal = Signal()
def __init__(self, parent = None):
QObject.__init__(self, parent)
@Slot()
def do_stuff(self):
print "[thread %x] computation started" % self.thread().currentThreadId()
for i in range(30):
# time.sleep(0.2)
x = 1000000
y = 100**x
print "[thread %x] computation ended" % self.thread().currentThreadId()
self.done_signal.emit()
class Example(QWidget):
def __init__(self):
super(Example, self).__init__()
self.initUI()
self.work_thread = QThread()
self.worker = Worker()
self.worker.moveToThread(self.work_thread)
self.work_thread.started.connect(self.worker.do_stuff)
self.worker.done_signal.connect(self.work_done)
def initUI(self):
self.btn = QPushButton('Do stuff', self)
self.btn.resize(self.btn.sizeHint())
self.btn.move(50, 50)
self.btn.clicked.connect(self.execute_thread)
self.setGeometry(300, 300, 250, 150)
self.setWindowTitle('Test')
self.show()
def execute_thread(self):
self.btn.setEnabled(False)
self.btn.setText('Waiting...')
self.work_thread.start()
print "[main %x] started" % (self.thread().currentThreadId())
def work_done(self):
self.btn.setText('Do stuff')
self.btn.setEnabled(True)
self.work_thread.exit()
print "[main %x] ended" % (self.thread().currentThreadId())
def main():
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
应用程序显示一个带有按钮的窗口。 按下按钮时,我希望它在执行计算时自行禁用。然后,该按钮应重新启用。
相反,当我按下按钮时,整个窗口在计算进行时冻结,然后,当它完成时,我重新获得对应用程序的控制。该按钮似乎从未被禁用。
我注意到的一件有趣的事情是,如果我用一个简单的 time.sleep() 替换 do_stuff()
中的 CPU 密集型计算,程序就会按预期运行。
我不完全知道发生了什么,但似乎第二个线程的优先级如此之高,以至于它实际上阻止了 GUI 线程被调度。如果第二个线程进入 BLOCKED 状态(就像 sleep()
发生的那样),GUI 实际上有机会按预期运行和更新界面。我尝试更改工作线程优先级,但在 Linux 上似乎无法完成。
另外,我尝试打印线程 ID,但我不确定我是否正确执行此操作。如果我是,线程亲和力似乎是正确的。
我还尝试了使用 PyQt 的程序,并且行为完全相同,因此标签和标题。如果我可以使用 PyQt4 而不是 PySide 运行它,我可以将整个应用程序切换到 PyQt4
【问题讨论】:
【参考方案1】:这可能是由持有 Python 的 GIL 的工作线程引起的。在某些 Python 实现中,一次只能执行一个 Python 线程。 GIL 阻止其他线程执行 Python 代码,并在不需要 GIL 的函数调用期间释放。
例如,在实际 IO 期间释放 GIL,因为 IO 由操作系统而不是 Python 解释器处理。
解决方案:
显然,您可以在您的工作线程中使用time.sleep(0)
来让步给其他线程(according to this SO question)。您必须自己定期调用time.sleep(0)
,GUI 线程只会在后台线程调用此函数时运行。
如果工作线程足够独立,你可以把它放到一个完全独立的进程中,然后通过管道发送腌制对象进行通信。在前台进程中,创建一个工作线程与后台进程做IO。由于工作线程将执行 IO 而不是 CPU 操作,因此它不会持有 GIL,这将为您提供一个完全响应的 GUI 线程。
某些 Python 实现(JPython 和 IronPython)没有 GIL。
CPython 中的线程仅对多路复用 IO 操作真正有用,而不是将 CPU 密集型任务置于后台。对于许多应用程序而言,CPython 实现中的线程从根本上被破坏了,并且在可预见的未来很可能会保持这种状态。
【讨论】:
我尝试在示例中添加time.sleep(0)
而不是time.sleep(0.2)
注释,不幸的是,并没有解决问题。我注意到对于诸如time.sleep(0.05)
之类的值,按钮的行为符合预期。作为一种解决方法,我可以将工作人员设置为每秒仅报告几次进度并休眠以让 GUI 自行更新。
有线程和 GUI 的解决方法(例如 code.activestate.com/recipes/578154)。
我最终这样做了:我在计算开始的地方放了一个sleep
,每次更新进度指示器时,我都会调用app.processEvents()
,以使“中止”按钮起作用。
这在所有可能的层面上都是根本错误的:Threads in CPython are only really useful for multiplexing IO operations, not for putting CPU-intensive tasks in the background.
否。基于 Qt 的 Python 应用程序在后台运行 CPU 密集型任务是琐碎 可实施。为什么?因为主 GUI 线程将其绝大多数时间片花费在纯 C++ Qt 层上,从而绕过了 Python 的 GIL。 GUI 应该始终在基于 Qt 的 Python 应用程序中响应。如果不是,则您不小心在主 GUI 线程中运行 CPU 密集型任务。
@CecilCurry:“在所有可能的层面上都存在根本错误”……我杀了你的狗还是什么?任何时候在后台处理 CPU 密集型任务时,都有可能不会释放 GIL。如果您在 Python 应用程序中使用 Qt,则 C++ 代码在回调 Python 时仍需要获取 GIL。如果 Qt 需要在您实际做某事时取回 GIL,那么 Qt 大部分时间都在没有 GIL 的情况下这一事实是无关紧要的。【参考方案2】:
最后,这适用于我的问题 - 代码可以帮助其他人。
import sys
from PySide import QtCore, QtGui
import time
class qOB(QtCore.QObject):
send_data = QtCore.Signal(float, float)
def __init__(self, parent = None):
QtCore.QObject.__init__(self)
self.parent = None
self._emit_locked = 1
self._emit_mutex = QtCore.QMutex()
def get_emit_locked(self):
self._emit_mutex.lock()
value = self._emit_locked
self._emit_mutex.unlock()
return value
@QtCore.Slot(int)
def set_emit_locked(self, value):
self._emit_mutex.lock()
self._emit_locked = value
self._emit_mutex.unlock()
@QtCore.Slot()
def execute(self):
t2_z = 0
t1_z = 0
while True:
t = time.clock()
if self.get_emit_locked() == 1: # cleaner
#if self._emit_locked == 1: # seems a bit faster but less responsive, t1 = 0.07, t2 = 150
self.set_emit_locked(0)
self.send_data.emit((t-t1_z)*1000, (t-t2_z)*1000)
t2_z = t
t1_z = t
class window(QtGui.QMainWindow):
def __init__(self):
QtGui.QMainWindow.__init__(self)
self.l = QtGui.QLabel(self)
self.l.setText("eins")
self.l2 = QtGui.QLabel(self)
self.l2.setText("zwei")
self.l2.move(0, 20)
self.show()
self.q = qOB(self)
self.q.send_data.connect(self.setLabel)
self.t = QtCore.QThread()
self.t.started.connect(self.q.execute)
self.q.moveToThread(self.t)
self.t.start()
@QtCore.Slot(float, float)
def setLabel(self, inp1, inp2):
self.l.setText(str(inp1))
self.l2.setText(str(inp2))
self.q.set_emit_locked(1)
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
win = window()
sys.exit(app.exec_())
【讨论】:
以上是关于PySide/PyQt - 启动 CPU 密集型线程会挂起整个应用程序的主要内容,如果未能解决你的问题,请参考以下文章
Pyside / Pyqt 从窗口打开新窗口(事件循环已在运行)
Python/pyside,pyqt(pyside,pyqt optional): 控制文本选择的函数
PySide/PyQt:PointingHandCursor 食谱?