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

问题描述 投票:0回答:3

我正在尝试在 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

python qt pyqt4 pyside qthread
3个回答
15
投票

这可能是工作线程持有Python的GIL造成的。在某些 Python 实现中,一次只能执行一个 Python 线程。 GIL 阻止其他线程执行 Python 代码,并在不需要 GIL 的函数调用期间释放。

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

解决方案:

  1. 显然,您可以在工作线程中使用

    time.sleep(0)
    来屈服于其他线程(根据这个SO问题)。您必须自己定期调用
    time.sleep(0)
    ,并且 GUI 线程仅在后台线程调用此函数时运行。

  2. 如果工作线程足够独立,您可以将其放入完全独立的进程中,然后通过管道发送pickle对象进行通信。在前台进程中,创建一个工作线程,与后台进程进行IO操作。由于工作线程将执行 IO 而不是 CPU 操作,因此它不会保留 GIL,这将为您提供完全响应的 GUI 线程。

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

CPython中的线程仅对多路复用 IO 操作真正有用,而不是将 CPU 密集型任务置于后台。对于许多应用程序来说,CPython 实现中的线程从根本上被破坏了,并且在可预见的未来可能会保持这种状态。


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

0
投票

如上所述,这是最适合您的解决方案:

If the worker thread is sufficiently self-contained, you can put it into a completely separate process, and then communicate by sending pickled objects over pipes. In the foreground process, create a worker thread to do IO with the background process. Since the worker thread will be doing IO instead of CPU operations, it won't hold the GIL and this will give you a completely responsive GUI thread.

© www.soinside.com 2019 - 2024. All rights reserved.