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

140
我正在编写一个GUI应用程序,通过网络连接定期检索数据。由于这个检索需要一段时间,在检索过程中会导致UI无响应(它不能被分成更小的部分)。这就是为什么我想将网络连接外包给一个单独的工作线程。
[是的,我知道,现在我有两个问题]
无论如何,该应用程序使用PyQt4,所以我想知道更好的选择是什么:使用Qt的线程还是使用Python的threading模块?每种方法的优缺点是什么?或者你有完全不同的建议吗?
编辑(关于奖励):虽然在我的特定情况下解决方案可能是使用像Jeff OberLukáš Lalinský建议的非阻塞网络请求(因此基本上将并发问题留给网络实现),但我仍然希望对一般问题有更深入的回答:
使用PyQt4(即Qt)线程与使用本地Python线程(来自threading模块)相比的优缺点是什么?

编辑2: 感谢大家的回答。虽然没有100%的一致意见,但似乎普遍认为答案是“使用Qt”,因为它的优点是与库的其他部分集成,而不会带来真正的缺点。

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

我考虑了几个赏金答案;最后我选择了abbot的,因为他提供了非常相关的外部参考资料;然而,这是一个接近的决定。

再次感谢。

7个回答

123

这个问题不久前在PyQt邮件列表中讨论过。引用Giovanni Bajo关于这个问题的评论

它们基本上是相同的。主要区别在于QThreads与Qt更好地集成(异步信号/槽,事件循环等)。此外,您无法从Python线程中使用Qt(例如,您无法通过QApplication.postEvent向主线程发送事件):您需要一个QThread才能使其工作。

一个经验法则可能是如果您要以某种方式与Qt交互,则使用QThreads,否则请使用Python线程。

还有一些关于此主题的早期评论来自PyQt的作者:“它们都是对相同本机线程实现的封装”。两种实现方式都以相同的方式使用GIL。


3
好的回答,但我认为你应该使用引用块按钮来清楚地显示你实际上不是在概括而是引用Giovanni Bajo的邮件列表 :) - c089
2
我想知道为什么你不能通过QApplication.postEvent()将事件发布到主线程,而需要使用QThread?我认为我见过有人这样做并且它可以工作。 - Trilarion
1
我已经在一个跨平台的应用程序中,以每秒100次的速率从Python线程中调用了QCoreApplication.postEvent,并且已经测试了数千小时。我从未看到过任何问题。只要目标对象位于MainThread或QThread中,我认为这是可以的。我还将其封装在一个很好的库中,参见qtutils - three_pineapples
5
鉴于这个问题和答案得到了高度赞同,我认为值得指出 ekhumoro 最近在 Stack Overflow 上的回答(链接见文章),详细说明了在哪些条件下可以安全地在 Python 线程中使用某些 Qt 方法。这与我自己和 @Trilarion 观察到的行为相符。 - three_pineapples

39

Python的线程会更简单和更安全,因为它适用于基于I/O的应用程序,所以它们能够绕过GIL。尽管如此,您考虑过使用Twisted或非阻塞socket/select进行非阻塞I/O吗?

编辑:关于线程的更多信息

Python线程

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

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

QT线程

当Python将控制权移交给第三方编译的模块时,它会释放GIL。确保必要时具有原子性成为该模块的责任。当控制权返回时,Python将使用GIL。这可能使得在使用线程与第三方库结合时变得困难。使用外部线程库更加困难,因为它会导致模块与解释器之间的控制权存在不确定性。

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库的包装器,为基于事件的编程提供了一个基本框架,并使用系统中最快的可用方法(在编译时确定)。


关于 Twisted 等:我使用一个第三方库来处理实际的网络操作;我想避免在其中进行补丁修复。但是我仍然会研究一下,谢谢。 - balpha
2
没有任何东西可以绕过GIL。但是Python在I/O操作期间释放GIL。当“移交”给编译的模块时,Python也会释放GIL,这些模块负责自己获取/释放GIL。 - Jeff Ober
2
更新只是错误的。Python 代码在 Python 线程和 QThread 中运行的方式完全相同。当您运行 Python 代码时,会获取 GIL(全局解释器锁),然后 Python 在线程之间管理执行,当您运行 C++ 代码时,会释放它。根本没有任何区别。 - Lukáš Lalinský
1
不,重点是无论你如何创建线程,Python解释器都不在乎。它关心的只是它能够获取GIL,并在X条指令之后释放/重新获取它。例如,您可以使用ctypes从C库创建回调,在单独的线程中调用该回调,而且代码即使不知道它是不同的线程也可以正常工作。线程模块真的没有什么特别的地方。 - Lukáš Lalinský
1
你之前提到 QThread 在锁定上的差异,以及如何“必须管理两套逻辑来锁定代码”。但我的观点是它并没有任何不同。我可以使用 ctypes 和 pthread_create 来启动线程,而且它会完全相同地工作。Python 代码根本不需要关心 GIL。 - Lukáš Lalinský
显示剩余6条评论

22

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

如果您要访问 HTTP 服务器,则应考虑使用 QNetworkAccessManager


1
除了我在Jeff Ober的回答中评论的内容之外,QNetworkAccessManager看起来很有前途。谢谢。 - balpha

15

当我在工作中使用PyTalk时,我也问过自己同样的问题。

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

通过信号/槽引擎,您将能够在一个线程之间以及您项目的每个部分进行通信。

此外,这种选择并没有非常关于性能的问题,因为两者都是C ++绑定。

以下是我在PyQt和线程方面的经验。

我鼓励您使用QThread


9

Jeff提出了一些好的观点。只有一个主线程可以进行任何GUI更新。如果您确实需要从线程内部更新GUI,Qt-4的queued connection信号使得在不同线程之间发送数据变得容易,并且如果您使用QThread,则会自动调用;我不确定如果您使用Python线程是否也会自动调用,但是可以很容易地通过向connect()添加参数来实现。


5
我不太能推荐其中任何一个,但我可以尝试描述CPython和Qt线程之间的区别。
首先,CPython线程不会并发运行,至少不是Python代码。是的,它们为每个Python线程创建系统线程,但只有当前持有全局解释器锁的线程被允许运行(C扩展和FFI代码可能会绕过它,但Python字节码在线程不持有GIL时不会执行)。
另一方面,我们有Qt线程,它们基本上是系统线程的通用层,没有全局解释器锁,因此能够并发运行。我不确定PyQt如何处理它,但除非您的Qt线程调用Python代码,否则它们应该能够并发运行(除了各种结构中可能实现的各种额外锁定)。
为了进行额外的微调,您可以修改解释的字节码指令数量,然后再切换GIL的所有权。较低的值意味着更多的上下文切换(可能会提高响应性),但每个单独线程的性能较低(上下文切换有其成本-如果您尝试每几个指令切换一次,则无法提高速度)。
希望这能帮助您解决问题 :)

8
这里需要注意:PyQt的QThreads会“获取全局解释器锁(GIL)”,因为所有Python代码都会锁定GIL,而你在PyQt中运行的任何QThread都将运行Python代码。(如果不是这样,你实际上并没有使用PyQt的“Py”部分 :)。如果你选择从Python代码转移到外部C库,则GIL将被释放,但无论你使用Python线程还是Qt线程,这一点都是正确的。 - quark
那其实就是我想要传达的意思,即所有的Python代码都会锁定,但是对于在单独线程中运行的C/C++代码来说这并不重要。 - p_l

0
我无法评论Python和PyQt线程之间的确切差异,但我一直在使用QThread、QNetworkAcessManager来尝试做你正在尝试做的事情,并确保在线程存活时调用QApplication.processEvents()。如果GUI响应性确实是您要解决的问题,后者将会有所帮助。

1
QNetworkAcessManager不需要线程或processEvents。它使用异步IO操作。 - Lukáš Lalinský
糟糕...是的,我正在使用 QNetworkAcessManagerhttplib2 的组合。我的异步代码使用了 httplib2 - brianz

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接