Python中PyQt多线程编程应该用QThread、QTimer还是threading?

按我的理解,由于 GIL 的存在,threading.Thread 肯定无法利用多核,任意时刻只有一个线程在跑; QThread 的话在 C++里肯定是能多个同时跑的,但在 PyQt 里它执行的也是 Python 代码,所以应该也和 threading 一样并不能真的并行;而 QTimer 似乎原本就是在创建它的那个线程里跑的,但既然大家都没法真的并行,那跟另外两个似乎是一回事儿,,而且 QTimer 写起来感觉更简洁、直观,也不用 sleep(),,所以我觉得写 PyQt 程序的时候并发逻辑用 QTimer 写就行了,,


Python中PyQt多线程编程应该用QThread、QTimer还是threading?

32 回复

qthread 是真线程。Python 只是包装,底下还是 c++在跑


核心答案:用 QThread。

PyQt 的多线程必须用 QThread,因为 GUI 操作必须在主线程(主事件循环)中执行。直接在其他线程(如 Python 标准库的 threading 线程)中操作 GUI 控件会导致程序崩溃或未定义行为。QThread 是 Qt 框架的线程类,它的事件循环与主线程的事件循环能安全通信。

为什么不是 threading? threading.Thread 创建的线程无法安全地与 Qt 的事件循环交互。如果你在子线程中直接调用例如 widget.setText(),程序大概率会崩溃。虽然可以用信号量或队列把数据传到主线程再更新 GUI,但这增加了复杂度,且不如 QThread 的信号槽机制直接、安全。

为什么不是 QTimer? QTimer 用于定时触发事件,它本身不创建新线程,定时器事件仍在主线程中处理。如果定时任务很耗时,会直接阻塞 GUI 响应。所以 QTimer 不适合用于需要并行计算的“多线程”场景,它只是单线程的定时工具。

QThread 的标准用法示例:

import sys
from PyQt5.QtCore import QThread, pyqtSignal
from PyQt5.QtWidgets import QApplication, QPushButton, QVBoxLayout, QWidget, QLabel

class WorkerThread(QThread):
    # 自定义信号,用于将结果传回主线程
    result_ready = pyqtSignal(str)

    def run(self):
        # 这里是耗时的任务,在子线程中运行
        import time
        for i in range(5):
            time.sleep(1)
            self.result_ready.emit(f"Step {i+1} completed")
        self.result_ready.emit("Done!")

class MainWindow(QWidget):
    def __init__(self):
        super().__init__()
        self.label = QLabel("Ready")
        self.button = QPushButton("Start Task")
        layout = QVBoxLayout()
        layout.addWidget(self.label)
        layout.addWidget(self.button)
        self.setLayout(layout)

        self.thread = WorkerThread()
        # 连接线程的信号到主线程的槽函数
        self.thread.result_ready.connect(self.update_label)
        self.button.clicked.connect(self.start_task)

    def start_task(self):
        self.button.setEnabled(False)
        self.label.setText("Running...")
        self.thread.start()

    def update_label(self, text):
        self.label.setText(text)
        if text == "Done!":
            self.button.setEnabled(True)

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = MainWindow()
    window.show()
    sys.exit(app.exec_())

总结:在 PyQt 里做多线程,老老实实用 QThread。



可是 QThread 的 run 函数里的代码都是 python 代码啊,,比如:
class TimeThread(QtCore.QThread):
def run(self):
# … …

v2ex 的回复真是,,,即不能修改、也不能预览,,


‘’’ python
class TimeThread(QtCore.QThread):
def run(self):
# … …
’’’

这个还真没仔细想过。但是我以前用过,你可以试试在线程里 sleep,看看主线程 ui 会卡住不



其实都不用试,要是真能通过这种方法实现真正的并行,,那大家就不会再提 GIL 的事情了,,

qt 的 runtime 都是 c++封装的,不必担心

类似于 run1(){PyEval_CallObject(run)}



只要你执行 Python 代码,,那 GIL 你就避不开,,

你这样一样得 Python 的虚拟机执行啊,,GIL 在虚拟机层面实现的。。

emmmmm 好像的确是这样,但属于 qt 框架本身的东西可以绕开 python 封装,worker 线程间可能会被 gil 影响,但 gui 的部分应该影响不着

当然了我也是猜的,确实没细想过这种问题

Jython and IronPython have no GIL and can fully exploit multiprocessor systems

PyPy currently has a GIL like CPython

in Cython the GIL exists, but can be released temporarily using a “with” statement

你怕不是对 python 的线程有什么误解吧。不管是 python 还是 Qt,任何线程里 sleep,其他线程都不可能卡住啊。Python 的线程也是货真价实的系统线程啊。只不过 python 的 GIL 做了解释器级别的同步,在非 IO 的情况下硬生生弄成了单线程,但本质上还是多线程。对于 sleep 这个操作来说,还是会真的挂起线程释放 CPU 到其他线程的。

QThread 的最大作用是可以发送信号, 利用信号-槽机制可以方便的与 GUI 跨线程交互, 从这个方面来抉择使用哪种线程,
用一个 QThread 来管理一堆 python 的线程或进程在 GUI 多线程编程中是一种比较方便的模式


用 QTimer 直接在同一个线程里,根本不用发信号,,岂不更方便。。

额,QTimer 是定时器,和多线程有啥关系。。。
给你一个复杂计算,界面不就卡死了了



嗯,好像有道理,,

不过大多数情况下用 QTimer 还是没问题的,,我好几个 PyQt 程序用 QTimer 替代多线程,运行良好

额。。。QTimer 与线程有啥关系,怎么能代替线程呢,你的程序能用只能说明代码有问题或者不需要多线程,还可能已经有异步实现
用 QThread 肯定躲不过 GIL,假设 PyQt 程序信号量超级多,肯定会堵塞,但是 PyQt 都是些小程序,到不了这个地步

你再仔细看看我说的呢,我的意思是在 QThread 里 sleep,这里只有 QThread,没有 Python 的 thread,要是代码实际上并没有在 QThread 里跑,那就应该卡住

其实说到“信号量超级多”这个问题,就算没有 GIL 他也一样会堵塞呀,因为 UI 线程还是只有一个,如果你要是在两个普通的 QThread 之间发送数据的话,那还是老老实实用 python 标准库的 queue 方便,反正都绕不过 GIL

用 QTimer 来完成复杂任务避免 GUI 堵塞是一种摒弃的做法了, 一点都不 OOP, 随着任务变多变复杂整个代码只会变得一团糟. 在两种情形下用过 QTimer: 1) GUI 初始化时, 用 QTimer.singleShot(0, load_data)来加载数据避免界面半天不出来, 2) 纯定时器.

对于 1), 如果预处理比较复杂, 我宁愿放到单独的预处理线程, 界面只用于数据输入, 数据展示, 处理信号 /事件. 这样界面就很流畅

This is the traditional way of implementing heavy work in GUI applications, but as multithreading is nowadays becoming available on more and more platforms, we expect that zero-millisecond QTimer objects will gradually be replaced by QThreads.

http://doc.qt.io/qt-5/qtimer.html

另外, 线程中有 sleep 的代码充满着坏味道

相对于 Qt 程序,pyQt 有 GIL,线程越多 UI 线程的时间片就越有限,堵塞的临界点就不一样了



感谢回复,,

可你也要考虑 PyQt 和 Qt 的区别,Qt 里面是有真正的并行执行的,,两个线程分别占一个 CPU 核、并行执行;这时候 QThread 当然比 QTimer 好 100 倍

可是 PyQt 里面由于 GIL 的问题,你开了 QThread 它和主线程也只是交替执行的,,并没有真正的并行,,这时候感觉线程和 QTimer 区别就没那么明显了

如果是要在 PyQt 程序中进行密集计算的话, 我会用一个 QThread 来管理一些进程, 或者干脆把密集计算部分用 C++来实现; 如果线程只是进行 IO 操作的话, PyQt 的 QThread 和 C++的 QThread 跑起来感觉不到任何差别, 甚至可以利用协程, 这比 C++的 QThread 有更好的并发.

用 QThread 不用 Timer 很大程度上是从设计上考虑, 用 QThread 可以形成更好的程序结构. 用 QThread 当线程 /进程管理者, 负责跟界面的交互, 而不是生成一堆 QThread 去完成具体的任务

其实吧,要是能在 c++中无缝操作 python 的 list 和 dict 就完美了,可惜 python 并没有提供官方的 c++接口

恩,可能有点误解了

为什么有这种需求呢?

有些时候希望把一些运算量大的部分用 c++写,但是数据源依然是来自 python 代码,而这个时候,如果需要处理大量数据,那么这些数据就一定是放在 list 或者 dict 中的,有些时候还要求返回的依然是一个 list,所以如果可以在 c++中像 std::vector 以及 std::map 中操作 list 和 map 那么互操作性就大大增加了
这点 Boost.Python 做的就挺好,但是整个 boost 类库有点过于庞大了,要是能拆分出来就完美了

"大量数据"要看具体类型, 数值数据用 list 存储是极其低效的, 可以用标准库的 array 或 numpy 存储, 想要避开 GIL 处理字符串可以在 Cython 中直接使用 C++的容器(string, vector, map 等, http://docs.cython.org/en/latest/src/userguide/wrapping_CPlusPlus.html ). array 和 numpy 也可以很容易把底层指针暴露给 C++处理

没实际用过 boost python, 感觉不如 Cython 简便直观, 有空深入学习下. 要拆分出来应该很容易, 只需要单独编译 boost python 库就行了 http://www.boost.org/doc/libs/1_66_0/libs/python/doc/html/building/installing_boost_python_on_your_.html

qtimer 不是多线程,它是把消息定期插入消息队列,消息队列各个消息是串行的。

是限制在单核上的多线程。因为有调度,执行顺序还是随机的。

很多情况下存在数据来源不可控的问题,原来的代码是使用纯 python 代码写的,自然也就没有用 array 和 numpy 的容器,而有些运算是需要做一些修改源数组的,这时候如果再写一部分衔接代码在两种数据结构之间拷贝就不太划来了

还是有区别的。比如 qtimer 里面长时间 io 操作会卡界面。而线程里不会,等 io 的时候它会让渡控制权。

再比如线程里面对字段或者全局变量的访问需要上锁,因为有调度,qtimer 就不用。

单核上的线程也是线程,不能假设它是严格串行的。

回到顶部