GUI过载是什么时候发生的?

3

假设您需要永久地在UI线程/调度程序上异步调用某个方法,使用以下方法:

while (true) {

    uiDispatcher.BeginInvoke(new Action<int, T>(insert_), DispatcherPriority.Normal, new object[] { });
}

在程序的每次运行中,你会观察到应用程序的GUI在大约90秒后开始冻结,由于调用过多而导致(时间变化但大约在1到2分钟之间)。
如何准确确定(测量?)这种过载发生的时间点,以便尽早停止它?
附录I:
在我的实际程序中,我没有无限循环。我有一个算法,在终止之前迭代了数百次。在每次迭代中,我都会向我的WPF应用程序中的列表控件添加一个字符串。我使用while (true) {...}构造,因为它最能匹配实际发生的情况。事实上,算法正确终止,并且所有(数百个)字符串都正确添加到我的列表中,但是一段时间后,直到算法终止后,我失去了使用GUI的能力-然后GUI又可以响应了。
附录II:
我的程序目的是观察特定算法的运行情况。我添加的字符串是日志条目:每次迭代一个日志字符串。我调用这些添加操作的原因是算法正在另一个线程中运行而不是UI线程中。为了适应不能从UI线程以外的任何线程进行UI操作的事实,我构建了一种ThreadSafeObservableCollection(但我非常确定这段代码不值得发布,因为它会削弱实际问题,我认为是UI无法处理重复和快速调用方法)。

6
你使用斜体字体有些分散注意力 - David B
1
请尝试使用 DispatcherPriority.Input。 - Danny Varod
看起来很像你之前的问题,答案也是一样的:GUI(图形用户界面)应该是面向用户的。从任意快速的进程中驱动它在设计上根本就是有缺陷的。 - H H
我仍然认为你缺少一款将“原始数据”转换为(更少的)“信息”以供用户使用的软件。这正是计算机存在的目的。 - H H
正如@DannyVarod建议的那样,您是否尝试使用DispatcherPriority.Input。这应该确保您自己的消息不会压倒UI线程,并且UI线程继续处理与呈现和处理用户输入相关的其他消息。您也可以尝试使用DispatcherTimer来发送您的消息(例如,您可以在每个计时器滴答声上排队大约1000条消息)。使用计时器,您应该能够限制消息发送频率。虽然在您的情况下,由于消息是在单独的线程上收集/准备的,因此需要更多的努力来正确实施。 - Amit Mittal
显示剩余6条评论
7个回答

2
很简单:当您过度使用用户的眼球时,您正在错误地执行它。对于现代CPU核心而言,超过每秒20次更新后,显示的信息就开始看起来像模糊了。这是电影利用的东西,电影以每秒24帧的速度播放。
更新速度比这更快只是浪费资源。在UI线程开始弯曲之前,您仍有大量的呼吸空间。这取决于您要求它执行的工作量,但典型的安全系数为x50。基于Environment.TickCount的简单定时器将完成此工作,在差异为>= 45毫秒时触发更新。

非常感谢您的回复。您指出的问题与@HenkHoltermann在上面的评论中所说的问题相同。您所说的听起来对我来说绝对合理,目前似乎我别无选择,只能采用您缩小要显示的数据的方法。为了实现这一点,我首先必须考虑重新定义我的需求 - 因为我认为您的方法可能无法满足它们。无论如何,再次感谢您的帮助 - 非常感激。 - marc wellman
取决于内容的类型。如果内容是图形化的并且包含快速变化,甚至每秒24帧可能都不够。 - Danny Varod

2

频繁地向UI发布消息是一个警告信号。这里有一个替代方案:将新字符串放入ConcurrentQueue中,并让定时器每100毫秒将它们取出。

非常简单易行,而且结果完美。


1
嗨,谢谢。这正是我所做的事情...在我几天前的帖子中,您可以看到更多的代码,包括我的延迟队列此处 - marc wellman
1
我也回答了你的另一个问题。我认为最好删除所有手动实现的队列、锁和监视器,只使用内置的ConcurrentQueue。每个UI控件和线程一个队列,每个队列一个计时器。 - usr

1
我没有使用过WPF,只用过Windows Forms,但是我建议,如果有一个仅需要被异步更新的视图控件,正确的做法是编写控件,使其属性可以自由地从任何线程访问,并且仅当没有更新挂起时才会BeginInvoke刷新例程;后者可以通过一个Int32“标志”和Interlock.Exchange来确定(属性设置器在更改基础字段后调用Interlocked.Exchange来设置标志;如果标志已清除,则对刷新例程执行BeginInvoke;刷新例程然后清除标志并执行刷新)。在某些情况下,该模式可能通过使控件的刷新例程检查自上次运行以来经过了多长时间,并且如果答案小于20ms左右,则使用计时器在上一次之后20ms触发刷新来进一步增强。

尽管.NET可以处理在UI线程上发布许多BeginInvoke操作,但对于单个控件而言,有多个更新挂起是没有意义的。将挂起的操作限制为每个控件一个(或最多一个小数量),就不会有队列溢出的危险。


感谢您详细的回复。 如何实现控件属性可以自由地从任何线程访问?我不需要至少一个线程来操作控件吗? - marc wellman

1
将supercat的解决方案以更符合WPF的方式进行尝试,尝试使用MVVM模式,然后您可以拥有一个单独的“视图模型”类,该类可以在线程之间共享,也许在适当的时候取出锁定或使用并发集合类。您实现一个接口(我认为是INotifyPropertyChanged),并触发一个事件来表示集合已更改。此事件必须从UI线程触发,但仅需要

感谢您的回复。我正在使用MVVM模式,并构建了一个专门的ThreadSaveObservableColection,它作为我的ViewModel的一部分。虽然它还没有完全完成,但您可以参考我之前关于此问题的一个帖子在这里 - marc wellman
是的,问题在于你的可观察集合实现没有任何帮助。它的问题在于你只是基于标准可观察集合并在 UI 线程上调用插入操作。你需要做的是将插入操作排队,并在大约每 400 毫秒进行一次批量处理。 - ForbesLindesay

1

好的,之前评论中链接有误,抱歉。但我继续阅读了一下,或许这会有所帮助:

The DispatcherOperation object returned by BeginInvoke can be used in several ways to interact with the specified delegate, such as:

Changing the DispatcherPriority of the delegate as it is pending execution in the event queue.
Removing the delegate from the event queue.
Waiting for the delegate to return.
Obtaining the value that the delegate returns after it is executed.

If multiple BeginInvoke calls are made at the same DispatcherPriority, they will be executed in the order the calls were made.

If BeginInvoke is called on a Dispatcher which has shut down, the status property of the returned DispatcherOperation is set to Aborted.

也许你可以处理一下你正在等待的代表人数...

现在我已经阅读了你的附录,我相信有一种更有效的方法来完成这个任务。 - mindandmedia
首先,非常感谢您的努力,我们非常感激。请您好心参考我的第二个附录。 - marc wellman

0

经过阅读其他人提供的答案以及您对它们的评论,您实际上的意图似乎是确保UI保持响应。为此,我认为您已经收到了很好的建议。

但是,为了逐字回答您的问题(如何检测和标记UI线程的过载),我可以提出以下建议:

  1. 首先确定什么是“重载”的定义(例如,我可以假设它是“UI线程停止渲染控件并停止处理用户输入”达到足够长的时间)
  2. 定义这个时间长度(例如,如果UI线程在最多40ms内继续处理渲染和输入消息,我会说它没有超载)。
  3. 现在使用根据您对超载的定义设置的DispatcherPriority启动一个DispactherTimer(对于我的例子,它可以是DispatcherPriority.Input或更低),并且间隔时间要比您的“超载”时间长度短得足够多
  4. 维护一个类型为DateTime的共享变量,并在计时器的每个tick中将其值更改为DateTime.Now。
  5. 在传递给BeginInvoke的委托中,您可以计算当前时间和上次Tick被触发的时间之间的差异。如果超过了您的“超载”测量标准,那么UI线程就会根据您的定义处于“超载”状态。然后,您可以设置一个共享标志,该标志可以从循环内部检查以采取适当的操作。

虽然我承认,这并不是绝对可靠的,但通过经验调整你的“度量”,你应该能够在它影响你之前检测到过载。


0

使用StopWatch来测量最小值、最大值、平均值、第一次和最后一次更新的持续时间。(您可以将其输出到您的UI界面。)

您的更新频率必须小于1/(平均更新持续时间)。

更改算法的实现方式,使其迭代由多媒体定时器调用,例如this .NET wrapperthis .NET wrapper。当定时器被激活时,使用Interlocked防止在当前迭代完成之前运行新的迭代。如果需要在主线程上进行迭代,请使用调度程序。您可以在定时器事件中运行多个迭代,使用参数和时间测量确定每个定时器事件要运行多少次迭代以及您想要定时器事件的频率。

我不建议使用少于5毫秒的计时器,因为计时器事件会使CPU窒息。

正如我在评论中写的那样,在将任务分派到主线程时,请使用DispatcherPriority.Input,这样UI的CPU时间就不会被分派占用。这是UI消息具有的相同优先级,因此它们不会被忽略。


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