PySide/PyQt - 启动 CPU 密集型线程会挂起整个应用程序 [英] PySide/PyQt - Starting a CPU intensive thread hangs the whole application

查看:68
本文介绍了PySide/PyQt - 启动 CPU 密集型线程会挂起整个应用程序的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在尝试在我的 PySide GUI 应用程序中做一件相当常见的事情:我想将一些 CPU 密集型任务委托给后台线程,以便我的 GUI 保持响应,甚至可以在计算过程中显示进度指示器.

I'm trying to do a fairly common thing in my PySide GUI application: I want to delegate some CPU-Intensive task to a background thread so that my GUI stays responsive and could even display a progress indicator as the computation goes.

这是我正在做的事情(我在 Python 2.7、Linux x86_64 上使用 PySide 1.1.1):

Here is what I'm doing (I'm using PySide 1.1.1 on 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()

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

The application displays a single window with a button. When the button is pressed, I expect it to disable itself while the computation is performed. Then, the button should be re-enabled.

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

What happens, instead, is that when I press the button the whole window freezes while the computation goes and then, when it's finished, I regain control of the application. The button never appears to be disabled. A funny thing I noticed is that if I replace the CPU intensive computation in do_stuff() with a simple time.sleep() the program behaves as expected.

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

I don't exactly know what's going on, but it appears that the second thread's priority is so high that it's actually preventing the GUI thread from ever being scheduled. If the second thread goes in BLOCKED state (as it happens with a sleep()), the GUI has actually the chance to run and updates the interface as expected. I tried to change the worker thread priority, but it looks like it can't be done on Linux.

此外,我尝试打印线程 ID,但我不确定我是否正确执行此操作.如果我是,线程关联似乎是正确的.

Also, I try to print the thread IDs, but I'm not sure if I'm doing it correctly. If I am, the thread affinity seems to be correct.

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

I also tried the program with PyQt and the behavior is exactly the same, hence the tags and title. If I can make it run with PyQt4 instead of PySide I could switch my whole application to PyQt4

推荐答案

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

This is probably caused by the worker thread holding Python's GIL. In some Python implementations, only one Python thread can execute at a time. The GIL prevents other threads from executing Python code, and is released during function calls that don't need the GIL.

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

For example, the GIL is released during actual IO, since IO is handled by the operating system and not the Python interpreter.

解决方案:

  1. 显然,您可以在工作线程中使用 time.sleep(0) 来让步给其他线程 (根据这个问题).您必须自己定期调用 time.sleep(0),并且 GUI 线程只会在后台线程调用此函数时运行.

  1. Apparently, you can use time.sleep(0) in your worker thread to yield to other threads (according to this SO question). You will have to periodically call time.sleep(0) yourself, and the GUI thread will only run while the background thread is calling this function.

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

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.

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

Some Python implementations (JPython and IronPython) do not have a GIL.

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

Threads in CPython are only really useful for multiplexing IO operations, not for putting CPU-intensive tasks in the background. For many applications, threading in the CPython implementation is fundamentally broken and it is likely to stay that way for the forseeable future.

这篇关于PySide/PyQt - 启动 CPU 密集型线程会挂起整个应用程序的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆