【问题标题】:Example of the right way to use QThread in PyQt?在 PyQt 中使用 QThread 的正确方法示例?
【发布时间】:2013-05-28 14:44:20
【问题描述】:

我正在尝试学习如何在 PyQt Gui 应用程序中使用 QThreads。我的东西可以运行一段时间,(通常)有可以更新 Gui 的点,但我想将主要工作分成自己的线程(有时东西会卡住,最终有一个很好的取消/重试按钮,如果 Gui 因为主循环被阻塞而被冻结,这显然不起作用)。

我读过https://mayaposch.wordpress.com/2011/11/01/how-to-really-truly-use-qthreads-the-full-explanation/。该页面说重新实现 run 方法不是这样做的方法。我遇到的问题是找到一个 PyQt 示例,它有一个执行 Gui 的主线程和一个不这样做的工作线程。这篇博客文章是针对 C++ 的,所以虽然它的例子确实有帮助,但我还是有点迷茫。有人可以指出一个在 Python 中正确执行此操作的示例吗?

【问题讨论】:

  • 这看起来像是 Background thread with QThread in PyQt 的副本。已接受答案中的第二个示例看起来像是您链接的博客中 C++ 代码的直接翻译。
  • 另外,您是否编写过任何本机 Python 线程代码(使用 threading.Thread 等)?如果没有,您可能需要先完成一些示例。 (另请参阅Threading in a PyQt application: Use Qt threads or Python threads,看看您是否还需要 QThread。)
  • @abarnert 谢谢,我认为该链接正是我想要的。我已经看到了第二个链接,并决定我应该使用 QThreads,因为我希望能够在线程之间发送槽/信号。我知道threading.Thread 存在,但之前没有使用过。我做了很多搜索,甚至看到了第一个链接,浏览了一下,看到了def run 并继续前进,没有意识到这两种方式都显示了!

标签: python pyqt pyqt4 qthread


【解决方案1】:

您说得对,在主线程执行 GUI 时让工作线程执行处理是一件好事。此外,PyQt 为线程检测提供了一种线程安全的信号/槽机制。

This may sound of interest。在他们的示例中,他们构建了一个 GUI

import sys, time
from PyQt4 import QtCore, QtGui

class MyApp(QtGui.QWidget):
 def __init__(self, parent=None):
  QtGui.QWidget.__init__(self, parent)

  self.setGeometry(300, 300, 280, 600)
  self.setWindowTitle('threads')

  self.layout = QtGui.QVBoxLayout(self)

  self.testButton = QtGui.QPushButton("test")
  self.connect(self.testButton, QtCore.SIGNAL("released()"), self.test)
  self.listwidget = QtGui.QListWidget(self)

  self.layout.addWidget(self.testButton)
  self.layout.addWidget(self.listwidget)

 def add(self, text):
  """ Add item to list widget """
  print "Add: " + text
  self.listwidget.addItem(text)
  self.listwidget.sortItems()

 def addBatch(self,text="test",iters=6,delay=0.3):
  """ Add several items to list widget """
  for i in range(iters):
   time.sleep(delay) # artificial time delay
   self.add(text+" "+str(i))

 def test(self):
  self.listwidget.clear()
  # adding entries just from main application: locks ui
  self.addBatch("_non_thread",iters=6,delay=0.3)

(包含一个列表小部件的简单用户界面,我们将通过单击按钮添加一些项目)

然后你可以创建我们自己的线程类,一个例子是

class WorkThread(QtCore.QThread):
 def __init__(self):
  QtCore.QThread.__init__(self)

 def __del__(self):
  self.wait()

 def run(self):
  for i in range(6):
   time.sleep(0.3) # artificial time delay
   self.emit( QtCore.SIGNAL('update(QString)'), "from work thread " + str(i) )

  self.terminate()

您确实重新定义了run() 方法。您可以找到 terminate() 的替代品,请参阅教程。

【讨论】:

  • OP 明确表示他想使用moveToThread 机制,而不是QThread.run 机制。我不确定他这样做是否有充分的理由,但你仍然没有回答他的问题。
  • OP 是对的:你不应该继承 QThread。见blog.qt.io/blog/2010/06/17/youre-doing-it-wrong
  • The Qt docs on threading 确实提到了 QThread 的子类化 ...
  • this blog 解释了为什么@blokeley 提到的部分错误。
  • 我建议将 run() 代码包装在 try/catch 块中。这是因为(至少在 Windows 和 py3.7 上)任何来自 run 方法的异常引发且未被捕获会导致应用程序静默关闭,而控制台中没有任何消息。
【解决方案2】:

这是一个单独的工作线程的工作示例,它可以发送和接收信号以允许它与 GUI 通信。

我制作了两个简单的按钮,一个在单独的线程中开始长时间计算,一个立即终止计算并重置工作线程。

在这里强制终止线程通常不是最好的处理方式,但在某些情况下,始终优雅退出不是一种选择。

from PyQt4 import QtGui, QtCore
import sys
import random

class Example(QtCore.QObject):

    signalStatus = QtCore.pyqtSignal(str)

    def __init__(self, parent=None):
        super(self.__class__, self).__init__(parent)

        # Create a gui object.
        self.gui = Window()

        # Create a new worker thread.
        self.createWorkerThread()

        # Make any cross object connections.
        self._connectSignals()

        self.gui.show()


    def _connectSignals(self):
        self.gui.button_cancel.clicked.connect(self.forceWorkerReset)
        self.signalStatus.connect(self.gui.updateStatus)
        self.parent().aboutToQuit.connect(self.forceWorkerQuit)


    def createWorkerThread(self):

        # Setup the worker object and the worker_thread.
        self.worker = WorkerObject()
        self.worker_thread = QtCore.QThread()
        self.worker.moveToThread(self.worker_thread)
        self.worker_thread.start()

        # Connect any worker signals
        self.worker.signalStatus.connect(self.gui.updateStatus)
        self.gui.button_start.clicked.connect(self.worker.startWork)


    def forceWorkerReset(self):      
        if self.worker_thread.isRunning():
            print('Terminating thread.')
            self.worker_thread.terminate()

            print('Waiting for thread termination.')
            self.worker_thread.wait()

            self.signalStatus.emit('Idle.')

            print('building new working object.')
            self.createWorkerThread()


    def forceWorkerQuit(self):
        if self.worker_thread.isRunning():
            self.worker_thread.terminate()
            self.worker_thread.wait()


class WorkerObject(QtCore.QObject):

    signalStatus = QtCore.pyqtSignal(str)

    def __init__(self, parent=None):
        super(self.__class__, self).__init__(parent)

    @QtCore.pyqtSlot()        
    def startWork(self):
        for ii in range(7):
            number = random.randint(0,5000**ii)
            self.signalStatus.emit('Iteration: {}, Factoring: {}'.format(ii, number))
            factors = self.primeFactors(number)
            print('Number: ', number, 'Factors: ', factors)
        self.signalStatus.emit('Idle.')

    def primeFactors(self, n):
        i = 2
        factors = []
        while i * i <= n:
            if n % i:
                i += 1
            else:
                n //= i
                factors.append(i)
        if n > 1:
            factors.append(n)
        return factors


class Window(QtGui.QWidget):

    def __init__(self):
        QtGui.QWidget.__init__(self)
        self.button_start = QtGui.QPushButton('Start', self)
        self.button_cancel = QtGui.QPushButton('Cancel', self)
        self.label_status = QtGui.QLabel('', self)

        layout = QtGui.QVBoxLayout(self)
        layout.addWidget(self.button_start)
        layout.addWidget(self.button_cancel)
        layout.addWidget(self.label_status)

        self.setFixedSize(400, 200)

    @QtCore.pyqtSlot(str)
    def updateStatus(self, status):
        self.label_status.setText(status)


if __name__=='__main__':
    app = QtGui.QApplication(sys.argv)
    example = Example(app)
    sys.exit(app.exec_())

【讨论】:

  • 当我运行这个时,我明白了。 Qt has caught an exception thrown from an event handler. Throwing exceptions from an event handler is not supported in Qt. You must reimplement QApplication::notify() and catch all exceptions there.
  • 知道异常是什么吗?我在 OS X 上使用 PyQt4 和 Python 2.7 测试了这个 sn-p,没有任何错误。当我尝试在 Python 3.4 中使用它时,我偶尔会遇到分段错误。我不确定这个问题是否与编写的代码或特定(特定于版本)的实现有关。
  • 我还没有调查什么是抛出,但显然你有一些东西偶尔会在消息循环中抛出异常,这绝不会发生。
  • 如果您尝试复制此示例,请务必注意:如果您在函数中创建 QThreadWorker,请务必同时存储它们。否则,在函数结束时,垃圾收集器将转储两者,Worker 将不会运行
  • 我发现我的示例存在问题,因为我试图在强制终止后重用线程和工作对象。这似乎是导致我看到的分段错误的原因。这不再是段错误,但有时在按下取消按钮并且应用程序挂起后线程没有终止。
【解决方案3】:

在我看来,到目前为止,最好的解释是 here 的示例代码,示例代码最初没有响应,然后得到改进。

请注意,这确实使用了所需的(非子类化)QThreadmoveToThread 方法,文章声称这是首选方法。

上面的链接页面还提供了相当于 C Qt 页面的 PyQt5,从 2011 年开始提供 definitive explanation by Maya Posch。我认为她当时可能正在使用 Qt4,但该页面仍然适用于 Qt5(因此 PyQt5)并且很好值得深入研究,包括许多 cmets(和她的回复)。

以防万一上面的第一个链接有一天变成 404(这太可怕了!),这是基本的 Python 代码,相当于 Maya 的 C 代码:

self.thread = QtCore.QThread()
# Step 3: Create a worker object
self.worker = Worker()
# Step 4: Move worker to the thread
self.worker.moveToThread(self.thread)
# Step 5: Connect signals and slots
self.thread.started.connect(self.worker.run)
self.worker.finished.connect(self.thread.quit)
self.worker.finished.connect(self.worker.deleteLater)
self.thread.finished.connect(self.thread.deleteLater)
self.worker.progress.connect(self.reportProgress)
# Step 6: Start the thread
self.thread.start()

# Final resets
self.longRunningBtn.setEnabled(False)
self.thread.finished.connect(
    lambda: self.longRunningBtn.setEnabled(True)
)
self.thread.finished.connect(
    lambda: self.stepLabel.setText("Long-Running Step: 0")
)   

NB self 在该页面的示例中是 QMainWindow 对象。我认为您可能必须非常小心将 QThread 实例附加到属性:实例超出范围时被销毁,但具有 QThread 属性,或者实际上是本地 QThread 实例超出范围,似乎能够导致一些莫名其妙的 Python 崩溃,sys.excepthook(或sys.unraisablehook)没有发现这些崩溃。建议谨慎。

...Worker 看起来像这样:

class Worker(QtCore.QObject):
    finished = QtCore.pyqtSignal()
    progress = QtCore.pyqtSignal(int)

    def run(self):
        """Long-running task."""
        for i in range(5):
            sleep(1)
            self.progress.emit(i + 1)
        self.finished.emit()

【讨论】:

    猜你喜欢
    • 2013-12-17
    • 2023-03-17
    • 1970-01-01
    • 1970-01-01
    • 2011-05-04
    • 1970-01-01
    • 2017-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多