ChatGPT解决这个技术问题 Extra ChatGPT

PyQt 应用程序中的线程:使用 Qt 线程还是 Python 线程?

我正在编写一个 GUI 应用程序,它定期通过 Web 连接检索数据。由于此检索需要一段时间,这会导致 UI 在检索过程中无响应(无法拆分为更小的部分)。这就是为什么我想将 Web 连接外包给一个单独的工作线程的原因。

[是的,我知道,现在我有 two problems。]

无论如何,该应用程序使用 PyQt4,所以我想知道更好的选择是:使用 Qt 的线程还是使用 Python threading 模块?每个的优点/缺点是什么?或者你有完全不同的建议?

编辑(重新赏金):虽然在我的特殊情况下的解决方案可能会使用建议的 Jeff OberLukáš Lalinský 之类的非阻塞网络请求(因此基本上将并发问题留给网络实现),我仍然希望对一般问题有更深入的回答:

与原生 Python 线程(来自 threading 模块)相比,使用 PyQt4(即 Qt)线程有哪些优点和缺点?

编辑2:谢谢大家的回答。尽管没有 100% 的一致意见,但似乎普遍认为答案是“使用 Qt”,因为这样做的优点是与库的其余部分集成,而不会造成真正的缺点。

对于希望在两种线程实现之间进行选择的任何人,我强烈建议他们阅读此处提供的所有答案,包括 abbot 链接到的 PyQt 邮件列表线程。

我为赏金考虑了几个答案;最后我选择了方丈作为非常相关的外部参考;然而,这是一个千钧一发的决定。

再次感谢。


a
abbot

这是不久前在 PyQt 邮件列表中的 discussed。引用 Giovanni Bajo 关于该主题的 comments

大部分是一样的。主要区别在于 QThreads 更好地与 Qt 集成(异步信号/插槽、事件循环等)。此外,您不能从 Python 线程使用 Qt(例如,您不能通过 QApplication.postEvent 将事件发布到主线程):您需要一个 QThread 才能工作。如果您打算以某种方式与 Qt 交互,一般的经验法则可能是使用 QThreads,否则使用 Python 线程。

PyQt 的作者之前对此主题的一些评论:“它们都是围绕相同的本机线程实现的包装器”。两种实现都以相同的方式使用 GIL。


很好的答案,但我认为您应该使用 blockquote 按钮来清楚地显示您实际上不是在总结,而是从邮件列表中引用 Giovanni Bajo :)
我想知道为什么你不能通过 QApplication.postEvent() 将事件发布到主线程并且需要一个 QThread 呢?我想我已经看到人们这样做了,并且奏效了。
我已经从 Python 线程以每秒 100 次的速度调用 QCoreApplication.postEvent,在一个跨平台运行的应用程序中已经测试了 1000 小时。我从未见过任何问题。我认为只要目标对象位于 MainThread 或 QThread 中就可以了。我还将它封装在一个不错的库中,请参阅 qtutils
鉴于这个问题和答案的投票率很高,我认为值得指出一个recent SO answer by ekhumoro,它详细说明了在哪些条件下可以安全地从 Python 线程中使用某些 Qt 方法。这与我和@Trilarion 观察到的行为一致。
J
Jeff Ober

Python 的线程将更简单、更安全,而且由于它是针对基于 I/O 的应用程序,它们能够绕过 GIL。也就是说,您是否考虑过使用 Twisted 或非阻塞套接字/选择的非阻塞 I/O?

编辑:更多关于线程

Python 线程

Python 的线程是系统线程。但是,Python 使用全局解释器锁 (GIL) 来确保解释器一次只执行一定大小的字节码指令块。幸运的是,Python 在输入/输出操作期间释放了 GIL,使线程可用于模拟非阻塞 I/O。

重要警告:这可能会产生误导,因为字节码指令的数量与程序中的行数不对应。在 Python 中,即使是单个赋值也可能不是原子的,因此对于必须以原子方式执行的任何代码块,即使使用 GIL,也需要互斥锁。

QT 线程

当 Python 将控制权交给第 3 方编译的模块时,它会释放 GIL。在需要时确保原子性成为模块的责任。当控制权被传回时,Python 将使用 GIL。这会使使用 3rd 方库和线程混淆。使用外部线程库更加困难,因为它增加了控制权在模块与解释器手中的位置和时间的不确定性。

QT 线程在 GIL 发布时运行。 QT 线程能够同时执行 QT 库代码(以及其他不获取 GIL 的编译模块代码)。但是,在 QT 线程的上下文中执行的 Python 代码仍然获取 GIL,现在您必须管理两组用于锁定代码的逻辑。

最后,QT 线程和 Python 线程都是系统线程的包装器。 Python 线程使用起来稍微安全一些,因为那些不是用 Python 编写的部分(隐式使用 GIL)在任何情况下都使用 GIL(尽管上面的警告仍然适用。)

非阻塞 I/O

线程给你的应用程序增加了异常的复杂性。尤其是在处理 Python 解释器和编译模块代码之间已经很复杂的交互时。虽然许多人发现基于事件的编程难以遵循,但基于事件的非阻塞 I/O 通常比线程更难推理。

使用异步 I/O,您始终可以确保,对于每个打开的描述符,执行路径是一致且有序的。显然,有一些问题必须解决,例如当代码依赖于一个开放通道时,该做什么进一步依赖于另一个开放通道返回数据时要调用的代码结果。

基于事件的非阻塞 I/O 的一个很好的解决方案是新的 Diesel 库。目前它仅限于 Linux,但它非常快速且非常优雅。

还值得您花时间学习 pyevent,它是一个出色的 libevent 库的包装器,它为使用系统最快的可用方法(在编译时确定)为基于事件的编程提供了一个基本框架。


Re Twisted 等:我使用第三方库来做实际的网络工作;我想避免修补它。但我还是会调查一下,谢谢。
实际上没有什么能绕过 GIL。但是 Python 在 I/O 操作期间会释放 GIL。 Python 在“移交”给编译模块时也会释放 GIL,这些模块负责自己获取/释放 GIL。
更新是错误的。 Python 代码在 Python 线程中的运行方式与在 QThread 中的运行方式完全相同。你在运行 Python 代码时获得 GIL(然后 Python 管理线程之间的执行),在运行 C++ 代码时释放它。完全没有区别。
不,关键是无论你如何创建线程,Python 解释器都不在乎。它所关心的是它可以获取 GIL 并且在 X 指令之后它可以释放/重新获取它。例如,您可以使用 ctypes 从 C 库中创建回调,该回调将在单独的线程中调用,并且代码可以正常工作,甚至不知道它是不同的线程。 thread 模块并没有什么特别之处。
您在说 QThread 在锁定方面有何不同,以及“您必须管理两组逻辑来锁定您的代码”。我要说的是,它根本没有什么不同。我可以使用 ctypes 和 pthread_create 来启动线程,它的工作方式完全相同。 Python 代码根本不需要关心 GIL。
L
Lukáš Lalinský

QThread 的优点是它与 Qt 库的其余部分集成在一起。也就是说,Qt 中的线程感知方法需要知道它们在哪个线程中运行,并且要在线程之间移动对象,您需要使用 QThread。另一个有用的功能是在线程中运行您自己的事件循环。

如果您正在访问 HTTP 服务器,则应考虑 QNetworkAccessManager


除了我对 Jeff Ober 的回答发表的评论外,QNetworkAccessManager 看起来很有希望。谢谢。
N
Natim

在为 PyTalk 工作时,我问过自己同样的问题。

如果您使用的是 Qt,则需要使用 QThread 才能使用 Qt 框架,尤其是信号/插槽系统。

使用信号/槽引擎,您将能够从一个线程到另一个线程以及项目的每个部分。

此外,由于两者都是 C++ 绑定,因此关于此选择的性能问题并不大。

这是我对 PyQt 和线程的体验。

我鼓励您使用 QThread


e
ekhumoro

杰夫有一些优点。只有一个主线程可以进行任何 GUI 更新。如果您确实需要从线程内更新 GUI,Qt-4 的 queued connection 信号使跨线程发送数据变得容易,并且如果您使用 QThread,它将自动被调用;如果您使用 Python 线程,我不确定它们是否会出现,尽管向 connect() 添加参数很容易。


p
p_l

我也不能真正推荐,但我可以尝试描述 CPython 和 Qt 线程之间的差异。

首先,CPython 线程不会并发运行,至少 Python 代码不会。是的,他们确实为每个 Python 线程创建了系统线程,但是只有当前持有全局解释器锁的线程才被允许运行(C 扩展和 FFI 代码可能会绕过它,但是当线程不持有 GIL 时不会执行 Python 字节码)。

另一方面,我们有 Qt 线程,它们基本上是系统线程的公共层,没有全局解释器锁,因此能够并发运行。我不确定 PyQt 如何处理它,但是除非您的 Qt 线程调用 Python 代码,否则它们应该能够同时运行(禁止可能在各种结构中实现的各种额外锁)。

对于额外的微调,您可以修改在切换 GIL 所有权之前解释的字节码指令的数量 - 较低的值意味着更多的上下文切换(并且可能更高的响应性)但每个线程的性能较低(上下文切换有其成本 - 如果你尝试每隔几条指令切换一次,这无助于加快速度。)

希望它对您的问题有所帮助:)


这里需要注意的是:PyQt QThreads 确实采用了全局解释器锁。所有 Python 代码都会锁定 GIL,并且您在 PyQt 中运行的任何 QThreads 都将运行 Python 代码。 (如果他们不这样做,您实际上并没有使用 PyQt 的“Py”部分 :)。如果您选择将该 Python 代码推迟到外部 C 库中,GIL 将被释放,但无论您使用 Python 线程还是 Qt 线程都是如此。
这实际上是我试图传达的,所有 Python 代码都获得了锁,但对于在单独线程中运行的 C/C++ 代码并不重要
b
brianz

我无法评论 Python 和 PyQt 线程之间的确切区别,但我一直在做您尝试使用 QThreadQNetworkAcessManager 做的事情,并确保在线程处于活动状态时调用 QApplication.processEvents()。如果 GUI 响应能力确实是您要解决的问题,the later 会有所帮助。


QNetworkAcessManager 不需要线程或 processEvents。它使用异步 IO 操作。
糟糕...是的,我正在使用 QNetworkAcessManagerhttplib2 的组合。我的异步代码使用 httplib2