PySide/PyQt - 启动一个CPU密集型线程会让整个应用程序挂起

16

我正在尝试在我的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。
2个回答

15

这可能是由于工作线程持有Python的GIL(全局解释器锁)造成的。在某些Python实现中,只有一个Python线程可以同时执行。GIL会防止其他线程执行Python代码,并且在不需要GIL的函数调用期间释放。

例如,在实际IO期间会释放GIL,因为IO由操作系统而不是Python解释器处理。

解决方案:

  1. 显然,您可以在工作线程中使用 time.sleep(0) 来让出对其他线程的控制权 (根据这个SO问题)。您将不得不自己定期调用 time.sleep(0),并且GUI线程仅在后台线程调用此函数时运行。

  2. 如果工作线程足够自包含,则可以将其放入完全独立的进程中,然后通过管道发送pickled对象进行通信。在前台进程中,创建一个工作线程与后台进程进行IO交互。由于工作线程将执行IO而不是CPU操作,因此它不会持有GIL,从而使您获得完全响应的GUI线程。

  3. 一些Python实现(JPython和IronPython)没有GIL。

在CPython中的线程只适用于多路复用IO操作,而不适用于将CPU密集型任务放到后台。对于许多应用程序,在CPython实现中,线程基本上是有缺陷的,而且在可预见的未来可能会保持这种状态。


我尝试在示例中将time.sleep(0.2)注释替换为time.sleep(0),但不幸的是,这并没有解决问题。我注意到,对于诸如time.sleep(0.05)之类的值,按钮的行为符合预期。作为一种解决方法,我可以设置工作程序每秒仅报告几次进度,并休眠以让GUI更新自身。 - user1491306
1
有关线程和GUI的解决方法(例如http://code.activestate.com/recipes/578154),可以采用一些变通方法。 - Noctis Skytower
我最终是这样做的:我在计算开始的地方放了一个 sleep,每次更新进度指示器时,我调用 app.processEvents() 以使“中止”按钮起作用。 - user1491306
1
这在所有可能的层面上都是根本错误的:CPython中的线程只对多路复用IO操作真正有用,而不是将CPU密集型任务放在后台。 不是这样的,基于Qt的Python应用程序在后台运行CPU密集型任务是非常容易实现的。为什么?因为主GUI线程在纯C++ Qt层花费了绝大部分时间片,从而规避了Python的GIL。在基于Qt的Python应用程序中,GUI应该始终保持响应。如果没有响应,则意味着您意外地在主GUI线程中运行CPU密集型任务。 - Cecil Curry
2
@CecilCurry:“在所有可能的层面上都是根本错误”…我是不是杀了你的狗?每当您在后台处理CPU密集型任务时,存在一定风险,可能无法释放GIL。如果您在Python应用程序中使用Qt,则C++代码仍然必须在调用回Python时获取GIL。如果Qt大部分时间都没有GIL,但实际执行操作时需要重新获取GIL,那么这个事实就不相关了。 - Dietrich Epp

0

最终这个代码解决了我的问题,希望能对其他人有所帮助。

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_())

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接