QThread 似乎没有启动; PyQt5,Python 2.7.9

Posted

技术标签:

【中文标题】QThread 似乎没有启动; PyQt5,Python 2.7.9【英文标题】:QThread doesn't appear to start; PyQt5, Python 2.7.9 【发布时间】:2016-07-01 06:55:40 【问题描述】:

摘要 PyQt5 似乎没有创建与 QThread 对象对应的新线程,或者我没有正确建立 Slot/Signal 链接。请帮我找出我的问题。


我是 Python 的一个相对随意的用户,但有人要求我为另一个团队创建一个实用程序,该实用程序将他们的一些 Python 库(它们本身包装 C++)包装在 GUI 中。因为这个实用程序是为另一个团队准备的,所以我不能更改编译器等的版本,或者至少,在没有提供正当理由的情况下不能更改。 该实用程序旨在提供一个接口,用于调试我的同事正在开发的某些硬件。

在检查了这些选项之后,我决定使用 Qt 和 PyQt 绑定。我遵循的步骤是:

    安装 Visual Studio 2010 SP1(必需,因为其他团队的库是使用此版本的 MS 编译器编译的)。 安装 Python 2.7.9(他们的 Python 版本) 安装qt-opensource-windows-x86-msvc2010-5.2.1.exe 获取 SIP-4.18.zip 的源代码并编译安装 获取 PyQt-gpl-5.2.1.zip 源码,编译安装 尝试构建一个封装了其他团队的通讯和翻译库的 PyQt 应用程序。据我所知,这些库并不是异步的,所以我认为我需要将应用程序的这一部分与 GUI 分开。

我编写的代码生成 UI,并且在某种意义上是响应式的,如果我在从 QAction 对象调用的方法中放置断点,那么这些断点就会被适当地触发。我的问题是我创建的 Worker 对象似乎没有移动到单独的线程(尽管调用了 moveToThread),因为如果我建立 BlockingQueuedConnection 而不是 QueuedConnection 类型的连接,那么我会遇到死锁。我在 Worker 类型的槽上放置的断点永远不会触发。

代码如下::

import os
import sys
import time

from PyQt5.QtWidgets import QMainWindow, QTextEdit, QAction, QApplication, QStatusBar, QLabel, QWidget, QDesktopWidget, QInputDialog
from PyQt5.QtGui import QIcon
from PyQt5.QtCore import Qt, QThread, QObject, pyqtSignal, pyqtSlot


class Worker(QObject):

    def __init__(self):
        super(Worker, self).__init__()
        self._isRunning = True
        self._connectionId = ""
        self._terminate = False

    @pyqtSlot()
    def cmd_start_running(self):
        """This slot is used to send a command to the HW asking for it to enter Running mode.
        It will actually work by putting a command in a queue for the main_loop to get to
        in its own serialised good time.  All the other commands will work in a similar fashion
        Up until such time as it is implemented, I will fake it."""
        self._isRunning = True
        pass

    @pyqtSlot()
    def cmd_stop_running(self):
        """This slot is used to send a command to the HW asking for it to enter Standby mode.
        Up until such time as it is implemented, I will fake it."""
        self._isRunning = False

    @pyqtSlot()
    def cmd_get_version(self):
        """This slot is used to send a command to the HW asking for its version string"""
        pass

    @pyqtSlot()
    def cmd_terminate(self):
        """This slot is used to notify this object that it has to join the main thread."""
        pass

    @pyqtSlot()
    def main_loop(self):
        """This slot is the main loop that is attached to the QThread object.  It has sleep periods
        that allow the messages on the other slots to be processed."""
        while not self._terminate:
            self.thread().sleep(1)
            # While there is stuff on the wire, get it off, translate it, then
            # signal it
            # For the mean while, pretend that _isRunning corresponds to when
            # RT streams will be
            # being received from the HW.
            if self._isRunning:
                pass
            # Search queue for commands, if any found, translate, then put on
            # the wire

class DemoMainWindow(QMainWindow):

    sgnl_get_version = pyqtSignal()
    sgnl_start_running = pyqtSignal()
    sgnl_stop_running = pyqtSignal()
    sgnl_terminate = pyqtSignal()


    def __init__(self):
        super(DemoMainWindow, self).__init__()
        self.initUI()

        self._workerObject = Worker()
        self._workerThread = QThread()
        self._workerObject.moveToThread(self._workerThread)
        self._workerThread.started.connect(self._workerObject.main_loop, type=Qt.QueuedConnection)


        # I changed the following connection to type BlockingQueuedConnection,
        # and got a Deadlock error
        # reported, so I assume that there is already a problem before I get to
        # this point.
        # I understand that the default for 'type' (Qt.AutoConnection) is
        # supposed to correctly infer that a QueuedConnection is required.
        # I was getting desperate.
        self.sgnl_get_version.connect(self._workerObject.cmd_get_version, type=Qt.QueuedConnection)
        self.sgnl_start_running.connect(self._workerObject.cmd_start_running, type=Qt.QueuedConnection)
        self.sgnl_stop_running.connect(self._workerObject.cmd_stop_running, type=Qt.QueuedConnection)
        self.sgnl_terminate.connect(self._workerObject.cmd_terminate, type=Qt.QueuedConnection)



    def initUI(self):

        textEdit = QTextEdit()
        self.setCentralWidget(textEdit)
        lbl = QLabel(self.statusBar())
        lbl.setText("HW Version:   ")
        self.statusBar().addPermanentWidget(lbl)

        exitAction = QAction(QIcon('exit24.png'), 'Exit', self)
        exitAction.setShortcut('Ctrl+Q')
        exitAction.setStatusTip('Exit application')
        exitAction.triggered.connect(self.close)

        connectAction = QAction(QIcon('connect24.png'), 'Connect', self)
        connectAction.setStatusTip('Connect to HW')
        connectAction.triggered.connect(self.establishCanConnection)

        enterRunningAction = QAction(QIcon('start24.png'), 'Start Running', self)
        enterRunningAction.setStatusTip('Start Running')
        enterRunningAction.triggered.connect(self.enterRunning)

        enterStandbyAction = QAction(QIcon('stop24.png'), 'Stop Running', self)
        enterStandbyAction.setStatusTip('Stop Running')
        enterStandbyAction.triggered.connect(self.enterStandby)

        self.statusBar()

        menubar = self.menuBar()
        fileMenu = menubar.addMenu('&File')
        fileMenu.addAction(exitAction)
        hwMenu = menubar.addMenu('&Hardware')
        hwMenu.addAction(connectAction)
        hwMenu.addAction(enterRunningAction)
        hwMenu.addAction(enterStandbyAction)

        toolbar = self.addToolBar('Exit')
        toolbar.addAction(exitAction)
        toolbar.addAction(connectAction)
        toolbar.addAction(enterRunningAction)
        toolbar.addAction(enterStandbyAction)

        self.setGeometry(300, 300, 400, 350) # x, y, width, height
        self.setWindowTitle('Demo Prog')
        self.show()

    def establishCanConnection(self):
        iDlg = QInputDialog(self)
        iDlg.setInputMode(QInputDialog.IntInput)
        idInt, ok = iDlg.getInt(self, 'CAN ID Selection', 'HW ID:')
        canID = '%s%d' % ('HW', idInt)
        if ok:
            self._workerThread.start()
            pass
        # this would be where the channel is established

    def enterRunning(self):
        self.sgnl_start_running.emit()
        # this would be where the command to start running is sent from

    def enterStandby(self):
        self.sgnl_stop_running.emit()
        # send the command to stop running


if __name__ == '__main__':

    app = QApplication(sys.argv)
    mainWindow = DemoMainWindow()
    sys.exit(app.exec_())

请注意,启动 _workerThread 的调用是在establishCanConnection 方法中,但这应该不是问题,不是吗? 我使用了 procmon 实用程序来检查是否创建了更多线程,如果运行了establishCanConnection,并且似乎有更多线程,但我发现很难将哪个线程(如果有的话)与 QThread 对象相关联。

【问题讨论】:

【参考方案1】:

除非你真的需要,否则不要使用BlockingQueuedConnection。如果你不知道你是否需要它,那么你就不需要它。

跨线程信号在接收线程的事件循环中排队。如果该线程正在运行阻塞的代码,它将无法处理任何事件。因此,如果您向被阻塞的线程发送带有BlockingQueuedConnection 的信号,您将遇到死锁。

您的示例使用了一个运行阻塞 while 循环的工作对象,因此它受到上述死锁问题的影响。如果您想向被阻塞的线程发送信号,则需要安排阻塞代码定期允许线程处理其事件,如下所示:

    while not self._terminate:
        self.thread().sleep(1)
        QApplication.processEvents()

PS:

如果要检查worker是否在不同的线程中运行,可以打印QThread.currentThread()QThread.currentThreadId()的返回值(这些函数是静态的,所以不需要QThread的实例打电话给他们)。

【讨论】:

我将接受这个作为答案。在我的问题中,我真的不想专注于 BlockingQueuedConnection - 这是一个我认为增加了工作对象不是真的在工作线程上的想法的实验。我错了。我从这个答案中得到的另外两件事是 1)QApplication.processEvents()(这是“解除阻塞”我的原因),以及 2)QThread.currentThreadId() 的额外好处。谢谢。

以上是关于QThread 似乎没有启动; PyQt5,Python 2.7.9的主要内容,如果未能解决你的问题,请参考以下文章

Python PyQt5 在 QThread 上为 QPushButton 加载图像

在 PyQt5 中取消 QThread

PyQt5中多线程模块QThread解决界面卡顿无响应问题,线程池ThreadPoolExecutor解决多任务耗时操作问题

PyQt5中多线程模块QThread解决界面卡顿无响应问题,线程池ThreadPoolExecutor解决多任务耗时操作问题

PyQt5中多线程模块QThread解决界面卡顿无响应问题,线程池ThreadPoolExecutor解决多任务耗时操作问题

在 QThread 中启动 QTimer