15

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

该应用程序显示一个带有按钮的窗口。按下按钮时,我希望它在执行计算时自行禁用。然后,应重新启用该按钮。

相反,当我按下按钮时,整个窗口在计算进行时冻结,然后,当它完成时,我重新获得对应用程序的控制。该按钮似乎从未被禁用。我注意到的一件有趣的事情是,如果我do_stuff()用一个简单的 time.sleep() 替换 CPU 密集型计算,则程序会按预期运行。

我不完全知道发生了什么,但似乎第二个线程的优先级如此之高,以至于它实际上阻止了 GUI 线程被调度。如果第二个线程进入 BLOCKED 状态(就像它发生在 a 中一样sleep()),则 GUI 实际上有机会按预期运行和更新界面。我试图改变工作线程的优先级,但它看起来无法在 Linux 上完成。

此外,我尝试打印线程 ID,但我不确定我是否正确执行此操作。如果我是,线程亲和力似乎是正确的。

我还尝试了使用 PyQt 的程序,并且行为完全相同,因此标签和标题。如果我可以使用 PyQt4 而不是 PySide 运行它,我可以将整个应用程序切换到 PyQt4

4

2 回答 2

14

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

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

解决方案:

  1. 显然,您可以time.sleep(0)在您的工作线程中使用以屈服于其他线程(根据this SO question)。您必须定期调用time.sleep(0)自己,GUI 线程只会在后台线程调用此函数时运行。

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

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

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

于 2012-06-29T17:03:12.077 回答
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_())
于 2016-10-08T18:07:08.907 回答