在PyQt应用中使用Qt线程还是Python线程?
我正在写一个图形用户界面(GUI)应用程序,它需要通过网络连接定期获取数据。因为这个获取数据的过程需要一些时间,所以在这个过程中,用户界面会变得无响应(无法把这个过程拆分成更小的部分)。所以我想把网络连接的工作放到一个单独的线程中去处理。
[是的,我知道,这样我现在有了两个问题。]
不过,这个应用程序是用PyQt4写的,所以我想知道更好的选择是什么:使用Qt的线程,还是使用Python的threading
模块?各自的优缺点是什么?或者你有没有其他的建议?
编辑(关于赏金):虽然在我这个特定的情况下,解决方案可能会使用像Jeff Ober和Lukáš Lalinský建议的非阻塞网络请求(基本上把并发问题留给网络实现),但我仍然想要一个更深入的答案,来回答这个一般性的问题:
使用PyQt4的(也就是Qt的)线程相比于原生Python线程(来自threading
模块),各自的优缺点是什么?
编辑2:感谢大家的回答。虽然没有100%一致的意见,但似乎大家普遍认为答案是“使用Qt”,因为这样做的好处是与库的其他部分集成,而没有真正的缺点。
对于那些在这两种线程实现之间犹豫的人,我强烈建议他们阅读这里提供的所有答案,包括abbot链接的PyQt邮件列表讨论。
我考虑了几个答案来作为赏金的选择;最后我选择了abbot的答案,因为它提供了非常相关的外部参考;不过这真的是个艰难的决定。
再次感谢大家。
7 个回答
QThread
的好处在于它和Qt库的其他部分紧密结合。也就是说,Qt中的一些方法需要知道它们在哪个线程中运行,而在不同线程之间移动对象时,你就需要用到QThread
。另一个有用的功能是可以在一个线程中运行你自己的事件循环。
如果你要访问一个HTTP服务器,建议使用QNetworkAccessManager
。
Python的线程会更简单和安全,因为它主要用于处理输入输出的应用,所以可以绕过全局解释器锁(GIL)。不过,你有没有考虑过使用Twisted或者非阻塞的套接字/选择来进行非阻塞的输入输出呢?
编辑:关于线程的更多内容
Python线程
Python的线程是系统线程。但是,Python使用全局解释器锁(GIL)来确保解释器一次只执行一小块字节码指令。幸运的是,在进行输入输出操作时,Python会释放GIL,这使得线程在模拟非阻塞输入输出时非常有用。
重要提示:这可能会让人误解,因为字节码指令的数量并不等同于程序中的行数。即使是一个简单的赋值操作,在Python中也可能不是原子操作,所以对于任何需要原子执行的代码块,即使有GIL,也需要使用互斥锁。
QT线程
当Python将控制权交给第三方编译模块时,它会释放GIL。此时,确保原子性的责任就落在了模块上。当控制权返回时,Python会再次使用GIL。这使得在使用线程时结合第三方库会变得有些混乱。使用外部线程库会更复杂,因为这会增加控制权在模块和解释器之间的切换的不确定性。
QT线程在释放GIL的情况下运行。QT线程能够并发执行QT库代码(以及其他不获取GIL的编译模块代码)。但是,在QT线程中执行的Python代码仍然会获取GIL,这样你就需要管理两套锁定逻辑。
总的来说,QT线程和Python线程都是系统线程的封装。使用Python线程相对安全一些,因为那些不是用Python编写的部分(隐式使用GIL)在任何情况下都会使用GIL(尽管上面的提示仍然适用)。
非阻塞输入输出
线程会给你的应用程序增加极大的复杂性。尤其是在处理Python解释器和编译模块代码之间已经复杂的交互时。虽然很多人觉得基于事件的编程难以理解,但基于事件的非阻塞输入输出通常比线程更容易理解。
使用异步输入输出,你可以确保对于每个打开的描述符,执行路径是一致和有序的。当然,也有一些问题需要解决,比如当依赖于一个打开通道的代码又依赖于另一个打开通道返回数据时该怎么办。
一个很好的基于事件的非阻塞输入输出的解决方案是新的Diesel库。目前它仅限于Linux,但速度非常快且相当优雅。
学习pyevent也是值得的,它是一个围绕出色的libevent库的封装,提供了一个基本的框架,用于使用系统中最快的方法进行基于事件的编程(在编译时确定)。
这个话题不久前在PyQt的邮件列表里讨论过。引用Giovanni Bajo在这个问题上的一些评论:
其实大同小异。主要的区别在于QThreads和Qt的结合得更好(比如异步信号/槽、事件循环等等)。另外,你不能在Python线程中使用Qt(比如你不能通过QApplication.postEvent向主线程发送事件):这需要用QThread才能实现。
一个简单的规则是,如果你打算和Qt进行某种交互,就用QThreads;如果没有这种需求,就用Python线程。
还有PyQt作者之前对此的评论:“它们都是对相同本地线程实现的封装”。而且这两种实现的GIL(全局解释器锁)使用方式是一样的。