如何在不“恶意”使用DoEvents()函数?

25

一次简单的DoEvents搜索会返回许多结果,基本上会说:

DoEvents很危险。不要使用它。请使用线程代替。

通常提到的原因有:

  • 可重入性问题
  • 性能差
  • 易用性问题(例如,在禁用窗口上拖放)

但是,一些著名的Win32函数,如TrackPopupMenuDoDragDrop执行自己的消息处理以保持UI响应,就像DoEvents一样。
然而,这些函数似乎没有遇到这些问题(性能、可重入性等)。

它们是如何做到的? 它们如何避免使用DoEvents所引用的问题?(或者确实存在问题?)


我刚在MSDN上找到了一篇非常有帮助的文章:https://blogs.msdn.microsoft.com/jfoscoding/2005/08/06/keeping-your-ui-responsive-and-the-dangers-of-application-doevents/ - Breeze
3个回答

33

DoEvents()是危险的。但我打赌你每天都会做很多危险的事情。就在昨天,我引爆了几个爆炸装置(未来的读者请注意原帖发布日期与某个美国节日的关系)。只要小心,我们有时可以考虑到这些危险。当然,这意味着知道和理解这些危险:

  • 重新进入问题。 实际上有两个危险:

    1. 这里的一部分问题与调用堆栈有关。如果在处理使用DoEvents()的消息的循环中调用.DoEvents(),以此类推,你会得到一个相当深的调用堆栈。过度使用DoEvents()并意外填满调用堆栈很容易,导致StackOverflow异常。如果你只在一个或两个地方使用.DoEvents(),那么你可能没问题。如果每次遇到长时间运行的进程都首先使用它,你很容易陷入麻烦。即使在错误的位置使用一次也可能使用户能够强制触发stackoverflow异常(有时只需按住回车键),这可能是一个安全问题。
    2. 有时可以在调用堆栈上找到相同的方法。如果你没有考虑到这一点(提示:你可能没有),那么坏事就会发生。如果传递给方法的所有内容都是值类型,并且没有依赖于方法外的东西,那么你可能没问题。但是,否则,你需要仔细考虑一下,在返回到调用.DoEvents()的位置之前,如果整个方法再次运行会发生什么。哪些参数或资源在你没有预期的情况下被修改了?你的方法是否改变了任何对象,其中堆栈上的两个实例可能在同一个对象上操作?
  • 性能问题。 DoEvents()可以给出多线程的假象,但它不是真正的多线程。这至少有三个真正的危险:

    1. 当你调用DoEvents()时,你将控制权交还给消息泵。消息泵可能会将控制权交给其他东西,而那个东西可能需要一段时间。结果是,你的原始操作可能需要比它自己在一个不会放弃控制的线程中运行所需的时间长得多。
    2. 工作的重复。由于有可能发现自己运行相同的方法两次,而我们已经知道这个方法是昂贵/长时间运行的(否则你就不需要DoEvents()),即使你考虑到了上面提到的所有外部依赖关系,以便没有不良副作用,你仍然可能最终重复大量工作。
    3. 另一个问题是第一个问题的极端版本:潜在死锁。如果你的程序中的其他东西取决于你的进程完成,并且将阻止直到它完成,而该东西是由DoEvents()的消息泵调用的,则你的应用程序将被卡住并变得无响应。这听起来可能有些牵强,但实际上很容易意外发生,并且后来的崩溃非常难以找到和调试。这是你自己电脑上可能遇到的一些挂起应用程序情况的根源。
  • 可用性问题。 这些是由于没有适当考虑其他危险而导致的副作用。如果已经适当地查看了其他地方,那么这里没有什么新内容。

"If"你能确保考虑到了所有这些问题,那么就继续吧。但是,如果DoEvents()是您解决UI响应/更新问题的首选位置,那么您可能没有正确考虑所有这些问题。如果它不是您寻找的第一个地方,那么有足够多的其他选项,我会质疑您如何考虑到DoEvents()。如今,DoEvents()主要用于与早期代码兼容,并作为新程序员的支撑,他们还没有获得足够的经验来接触其他选项。
事实上,在.Net世界中,大多数情况下,一个BackgroundWorker组件几乎和DoEvents()一样简单,至少在您做过一两次之后,它可以安全地完成工作。最近,使用async/await模式或使用Task可以更加有效和安全,而无需自己深入研究多线程代码。

3
你的主消息循环(main message loop)在模态循环(modal loop)中分发消息时没有被使用,因此你可能执行的所有特殊操作(比如MsgWaitForMultipleObjects或空闲处理)都将等到模态循环结束才会执行。 - Ben Voigt
我在回答这个问题时添加了一个关于可重入性问题的注释。 - Deanna
2
当然,如果不小心的话,线程编程也可能是危险的。 - Yuhong Bao

4

在16位Windows时代,当每个任务共享一个线程时,保持程序在紧密循环中的响应性的唯一方法是使用DoEvents。然而,这种非模态的用法已被弃用,而推荐使用线程。以下是一个典型的例子:

' Process image
For y = 1 To height
    For x = 1 to width
        ProcessPixel x, y
    End For
    DoEvents ' <-- DON'T DO THIS -- just put the whole loop in another thread
End For

对于模态内容(例如追踪弹出窗口),仍然很可能是可以的。

3
我可能错了,但我认为DoDragDrop和TrackPopupMenu是相当特殊的情况,因为它们接管了UI,所以没有重新进入问题(我认为这是人们描述DoEvents为“邪恶”的主要原因)。
个人认为将特性贬低为“邪恶”并不有益- 相反,应该解释其缺陷,以便人们自行决定。在DoEvents的情况下,还存在一些罕见情况可以使用它,例如显示模态进度对话框时,在此过程中用户无法与其余UI交互,因此不存在重新进入问题。
当然,如果您认为“邪恶”意味着“不完全理解陷阱就不应使用”,那么我同意DoEvents是邪恶的。

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