QThread.currentThreadId()的正确使用方法

9 投票
1 回答
11752 浏览
提问于 2025-04-18 05:17

我原以为要确定正在运行某个函数的 QThread 的 ID,只需要用 QThread.currentThreadId() 就可以了。然而,我发现这个方法的结果并不如我预期(在 PyQt5 和 Python 3 中;但我没有理由相信在 PyQt4 或 Python 2 中会有所不同,所以我用了通用标签)。线程 ID 的变化让我感到困惑,这表明我不能真正使用它,而 QThread 实例的 ID 则是可以预测的,这说明我应该用这个来识别当前正在运行的线程。为了测试这一点,我创建了以下代码:

from PyQt5 import QtCore, QtWidgets
from PyQt5.QtCore import pyqtSignal
import time
import sys

def logthread(caller):
    print('%-25s: %s, %s' % (caller, QtCore.QThread.currentThread(), QtCore.QThread.currentThreadId()))


class Worker(QtCore.QObject):
    done = pyqtSignal()

    def __init__(self, parent=None):
        logthread('worker.__init__')
        super().__init__(parent)

    def run(self, m=10):
        logthread('worker.run')
        for x in range(m):
            y = x + 2
            time.sleep(0.001) 
        logthread('worker.run finished')

        self.done.emit()


class MainWindow(QtWidgets.QWidget):
    def __init__(self, parent=None):
        logthread('mainwin.__init__')
        super().__init__(parent)

        self.worker = Worker()
        self.workerThread = None

        self.btn = QtWidgets.QPushButton('Start worker in thread')
        self.btn2 = QtWidgets.QPushButton('Run worker here')
        layout = QtWidgets.QVBoxLayout(self)
        layout.addWidget(self.btn)
        layout.addWidget(self.btn2)

        self.run()

    def run(self):
        logthread('mainwin.run')

        self.workerThread = QtCore.QThread()
        self.worker.moveToThread(self.workerThread)
        self.worker.done.connect(self.workerDone)
        self.btn.clicked.connect(self.worker.run)
        self.btn2.clicked.connect(self.runWorkerHere)

        self.workerThread.start()
        self.show()

    def workerDone(self):
        logthread('mainwin.workerDone')

    def runWorkerHere(self):
        logthread('mainwin.runWorkerHere')
        self.worker.run()


if __name__ == '__main__':
    app = QtWidgets.QApplication([])
    logthread('main')

    window = MainWindow()
    sys.exit(app.exec_())

当你运行这段代码时,前四行打印的内容是在进入事件循环之前的,这显示了 QThread.currentThread() 的 Python ID 在多个地方是不同的,但 QThread.currentThreadId() 是相同的:

main                     : <PyQt5.QtCore.QThread object at 0x01ABDD00>, <sip.voidptr object at 0x01A4ABC0>
mainwin.__init__         : <PyQt5.QtCore.QThread object at 0x01ABDD50>, <sip.voidptr object at 0x01A4ABC0>
worker.__init__          : <PyQt5.QtCore.QThread object at 0x01ABDDA0>, <sip.voidptr object at 0x01A4ABC0>
mainwin.run              : <PyQt5.QtCore.QThread object at 0x01ABDE90>, <sip.voidptr object at 0x01A4ABC0>

我本以为所有的 QThread Python ID 应该是相同的,但也许多个 QThread 实例包裹了同一个 C++ 线程指针,这也说得通。

现在点击“在这里运行工作者”按钮:这只是直接从 GUI 线程调用 worker.run 方法,所以这个方法应该表明它是在那个线程中运行的。这会打印出以下四行:

mainwin.runWorkerHere    : <PyQt5.QtCore.QThread object at 0x01ABDEE0>, <sip.voidptr object at 0x01A4ABC0>
worker.run               : <PyQt5.QtCore.QThread object at 0x01ABDEE0>, <sip.voidptr object at 0x01A4ABC0>
worker.run finished      : <PyQt5.QtCore.QThread object at 0x01ABDEE0>, <sip.voidptr object at 0x01A4ACC8>
mainwin.workerDone       : <PyQt5.QtCore.QThread object at 0x01ABDEE0>, <sip.voidptr object at 0x01A4ABC0>

确实,这次所有行的 QThread 实例 ID 是相同的,看到这一点真不错。但在第三行中,线程 ID 是不同的,这一行是由信号在 worker.run 中的槽打印出来的,然而信号是在同一个线程中生成的!这也意味着同一个 QThread 对象可以有多个底层线程 ID。

现在点击“启动工作者”。这会在工作者的线程中调用 worker.run。打印出的三行是:

worker.run               : <PyQt5.QtCore.QThread object at 0x01ABDE90>, <sip.voidptr object at 0x01A4ABC0>
worker.run finished      : <PyQt5.QtCore.QThread object at 0x01ABDE90>, <sip.voidptr object at 0x01A4ACC8>
mainwin.workerDone       : <PyQt5.QtCore.QThread object at 0x01ABDEE0>, <sip.voidptr object at 0x01A4ACC8>

worker.run 中(前两行)的 QThread 实例 ID 与槽中的不同,这很有道理。同样,线程 ID(sip.voidptr)的变化让我感到困惑:我本以为第二行会显示线程 ID 0x01A4ABC0,和第一行相同,而不是和第三行(槽)相同。

有趣的是,如果你把 logthread 格式中的线程 ID 输出替换为 QtWidgets.QApplication.instance().thread(),你会发现 QThread 实例 ID 在应用程序的 QThread 实例 ID 中是始终相同的,除非 worker.run 在单独的线程中运行。甚至在应用程序事件循环开始之前(换句话说,应用程序线程 ID 只有在事件循环开始后才会变得恒定)。

如果我测试得没错,上述内容表明 QThread 实例 ID 一旦 QApplication 事件循环开始,就有一致且可预测的值,但线程 ID(currentThreadId())则没有。因此,每当我想测试某个函数运行的线程位置时,我应该使用 QThread.currentThread(),并可能与 app.thread() 进行比较,但我应该避免使用 currentThreadId()。有人对我测试的方式和结论有问题吗?如果没有问题,这和 currentThreadId() 的文档有什么关系?如果我犯了错误,我错在哪里?

1 个回答

11

你的问题主要是因为你没有把返回的sip.voidptr转换成整数。如果你打印 int(QThread.currentThreadId()),你会得到有意义的数字。简单来说,你之前看到的是线程ID存储的内存地址,这个地址显然会根据应用程序当前的内存使用情况而变化。不过,这些内存地址的内容总是保持一致的。

你可能还会想知道,Python的线程模块也能给你相同的一致信息(见下面的例子)。

最后,我觉得你的应用程序不安全,因为你把 self.worker 对象移动到了QThread中,然后在主线程中直接调用了“运行工作者”的方法。在我下面的例子中,我为了安全起见,实例化了一个新的工作者对象。

另外,请原谅我把你的例子转换成了PyQt4和Python 2.7!

from PyQt4 import QtCore, QtGui
from PyQt4.QtCore import pyqtSignal
import time
import sys
import threading

def logthread(caller):
    print('%-25s: %s, %s,' % (caller, QtCore.QThread.currentThread(), int(QtCore.QThread.currentThreadId())))
    print('%-25s: %s, %s,' % (caller, threading.current_thread().name, threading.current_thread().ident))


class Worker(QtCore.QObject):
    done = pyqtSignal()

    def __init__(self, parent=None):
        logthread('worker.__init__')
        super(Worker, self).__init__(parent)

    def run(self, m=10):
        logthread('worker.run')
        for x in range(m):
            y = x + 2
            time.sleep(0.001) 
        logthread('worker.run finished')

        self.done.emit()


class MainWindow(QtGui.QWidget):
    def __init__(self, parent=None):
        logthread('mainwin.__init__')
        super(MainWindow, self).__init__(parent)

        self.worker = Worker()
        self.workerThread = None

        self.btn = QtGui.QPushButton('Start worker in thread')
        self.btn2 = QtGui.QPushButton('Run worker here')
        layout = QtGui.QVBoxLayout(self)
        layout.addWidget(self.btn)
        layout.addWidget(self.btn2)

        self.run()

    def run(self):
        logthread('mainwin.run')

        self.workerThread = QtCore.QThread()
        self.worker.moveToThread(self.workerThread)
        self.worker.done.connect(self.workerDone)
        self.btn.clicked.connect(self.worker.run)
        self.btn2.clicked.connect(self.runWorkerHere)

        self.workerThread.start()
        self.show()

    def workerDone(self):
        logthread('mainwin.workerDone')

    def runWorkerHere(self):
        logthread('mainwin.runWorkerHere')
        worker = Worker()
        worker.done.connect(self.workerDone)
        worker.run()
        # self.worker.run()


if __name__ == '__main__':
    app = QtGui.QApplication([])
    logthread('main')

    window = MainWindow()
    sys.exit(app.exec_())

撰写回答