为什么应该避免嵌套QEventLoops?

11
在他的Qt事件循环、网络和I/O API讲座中,Thiago Macieira提到应避免嵌套QEventLoop

QEventLoop用于嵌套事件循环...如果可以的话,请避免使用它,因为它会创建一些问题:可能会重新进入,出现未预期的套接字或计时器新激活。

有人能详细说明他所指的是什么吗?我维护了很多使用模态对话框的代码,当调用exec()时,内部会嵌套一个新的事件循环,因此我非常想知道这可能导致哪些问题。
2个回答

12
  1. 嵌套事件循环会占用1-2kb的堆栈。它在典型的32kb L1缓存CPU上占用5%的L1数据缓存,可能略有不同。

  2. 它具有重新进入已经在调用堆栈上的任何代码的能力。没有任何保证这些代码中的任何一个都是设计为可重入的。我谈论的是你的代码,而不是Qt的代码。它可以重新进入启动此事件循环的代码,并且除非你显式地控制这种递归,否则无法保证你最终不会耗尽堆栈空间。

  3. 在当前的Qt中,由于长期存在的API错误或平台不足,在两个地方必须使用嵌套的execQDrag和平台文件对话框(在某些平台上)。你不需要在其他任何地方使用它。对于非平台模态对话框,你不需要嵌套事件循环。

  4. 重新进入事件循环通常是由编写伪同步代码引起的,其中人们抱怨缺少yield()co_yieldco_await现在已经加入到C++中!),将头埋在沙子里,使用exec()代替。这种代码通常最终成为难以理解的意大利面条式代码,是不必要的。

    对于现代C ++,使用C ++ 20协程是值得的;周围有一些基于Qt的实验,易于构建。

    有一些Qt本地实现的堆栈协程:Skycoder42/QtCoroutings - 最近的项目,以及较旧的ckamm/qt-coroutine。我不确定后者的代码有多新鲜。看起来它们在某个时候都能正常工作。

    编写无需协程的异步代码通常通过状态机完成,例如此答案,以及与QStateMachine不同的实现QP框架

个人经历:我迫不及待地想让C++协程变得可用于生产环境,现在我使用golang编写异步通信代码,并将其静态链接到Qt应用程序中。效果很好,垃圾回收器几乎不会影响性能,而且与使用C++协程相比,代码更易于阅读和编写。我曾经使用C++协程TS编写了很多代码,但现在全部转移到了golang,并且没有后悔过。

谢谢你的回答。你对于“你没有预料到的套接字或定时器的新激活”有什么想法吗? - MuchToLearn
@MuchToLearn 这就是所谓的重新进入代码。通常情况下,您调用exec的代码可以再次被调用。这些事件的来源可能是UI,但也可能是定时器、套接字、本地平台事件等。 - Kuba hasn't forgotten Monica
状态机就像是缺失的范式,来补充面向对象、函数式和声明式编程。 - mip

0

嵌套的事件循环会导致顺序反转。(至少在qt4上是这样)

假设您有以下发生的事情序列:

enqueued in outer loop: 1,2,3
processing 1 => spawn inner loop
enqueue 4 in inner loop
processing 4
exit inner loop
processing 2

所以你可以看到处理顺序是:1、4、2、3。

我从经验中得知,这通常会导致我的代码崩溃。


有趣。我猜你的代码中存在一个排序约束,导致它崩溃了?你能详细说明一下这些事件是什么类型吗?我不明白为什么在内部循环中将事件4排队会使其首先被处理或导致代码崩溃。 - MuchToLearn
当然,代码依赖于某些消息序列。指针将在第一个事件上初始化,并在第二个事件上调用对象并崩溃,导致段错误。如果事物不是幂等的,依赖于顺序是有效的。 - mhstnsc
@MuchToLearn 最近在我的团队中出现了一个问题,因为有人想发起一个HTTP请求,并希望在内部循环中等待结果。最终他决定只是阻塞事件循环,如果不是UI相关的话,否则事情可能会变得混乱,因为引入异步步骤对API来说会相当具有传染性,但如果你使用Qt,那么就准备好付出代价吧 :) - mhstnsc

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