我正在尝试在我的PySide GUI应用程序中完成一个相当普遍的任务:我想将一些CPU密集型任务委托给后台线程,以使我的GUI保持响应并且甚至可以显示进度指示器随着计算的进行。
这是我正在做的事情(我正在使用PySide 1.1.1和Python 2.7,Linux x86_64):
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()
应用程序显示一个带有按钮的窗口。当按下按钮时,期望它在计算执行期间被禁用,然后重新启用。相反的情况是,在执行计算时整个窗口都会冻结,直到计算完成才能重新控制应用程序。按钮似乎从未禁用过。 我注意到的有趣的一点是,如果我将
do_stuff()
中的计算替换为简单的time.sleep()
,程序会按预期运行。虽然我不确定发生了什么,但似乎第二个线程的优先级非常高,以至于它实际上阻止了GUI线程的调度。如果第二个线程进入阻塞状态(如使用
sleep()
时),GUI实际上有机会运行并更新界面。我尝试更改工作线程的优先级,但在Linux上好像无法做到。我还尝试打印线程ID,但不确定是否正确。如果我正确的话,线程关联似乎是正确的。
我也尝试过使用PyQt,并且行为完全相同,因此使用了相同的标签和标题。如果我可以让它在PyQt4而不是PySide上运行,我可以将整个应用程序切换到PyQt4。
time.sleep(0.2)
注释替换为time.sleep(0)
,但不幸的是,这并没有解决问题。我注意到,对于诸如time.sleep(0.05)
之类的值,按钮的行为符合预期。作为一种解决方法,我可以设置工作程序每秒仅报告几次进度,并休眠以让GUI更新自身。 - user1491306sleep
,每次更新进度指示器时,我调用app.processEvents()
以使“中止”按钮起作用。 - user1491306CPython中的线程只对多路复用IO操作真正有用,而不是将CPU密集型任务放在后台。
不是这样的,基于Qt的Python应用程序在后台运行CPU密集型任务是非常容易实现的。为什么?因为主GUI线程在纯C++ Qt层花费了绝大部分时间片,从而规避了Python的GIL。在基于Qt的Python应用程序中,GUI应该始终保持响应。如果没有响应,则意味着您意外地在主GUI线程中运行CPU密集型任务。 - Cecil Curry