C# silverlight 中的 DispatcherTimer 和 UI 刷新限制
我再次为一个对你们所有人来说可能很简单的问题表示歉意。我对 Silverlight 幕后的了解有限。
我有一个图表应用程序 (Visiblox),我将其用作滚动范围,每 20 毫秒更新一次,添加和删除一个点。在伪代码中:
List<Point> datapoints= new List<Point>();
Series series = new Series(datapoints);
void timer_tick(){
datapoints.Add(new Point);
datapoints.RemoveAt(0);
// no need to refresh chart, it does refresh automatically
}
当在此图表工具中运行 6 系列时,它开始显示有点迟缓。将刻度更改为 10 毫秒没有任何区别,图表以相同的速度更新,因此似乎 20 毫秒是速度限制(UI 或图表?)。
我尝试使用 CompositionTarget.Rendering
并得到了相同的结果:低于 20 毫秒,速度没有差异。
然后我不小心同时启用了两者,速度加倍了。因此,我使用多个线程(2、3、4)进行了测试,并将速度提高了一倍、三倍和四倍。这还没有锁,因为我什至不知道需要在哪个进程上生成锁,但没有数据损坏或内存泄漏。
我的问题是,为什么 20 毫秒时运行缓慢的图表无法以 10 毫秒运行,但在多线程时却快得离谱? UI 刷新过程是否运行得更快?图表计算是否加倍?或者单个 DispatcherTimer 的执行速度是否有限制?
谢谢!
编辑:我有嵌入式编码的背景,所以当我想到线程和计时时,我立即想到切换硬件中的引脚并连接示波器来测量进程长度。我对 C# 中的线程很陌生,并且没有用于连接范围的引脚。有没有办法以图形方式查看线程计时?
Again I apologize for a question that might be simple to all of you. I have a limited understanding of what goes behind the scenes in Silverlight.
I have a charting app (Visiblox) that I use as a rolling scope updated every 20ms, adding and removing a point. In pseudocode:
List<Point> datapoints= new List<Point>();
Series series = new Series(datapoints);
void timer_tick(){
datapoints.Add(new Point);
datapoints.RemoveAt(0);
// no need to refresh chart, it does refresh automatically
}
When running 6 series in this charting tool, it started to show a bit sluggish. Changing the tick to 10ms made no difference whatsoever, chart was updated at the same speed, so it seems that 20ms is the speed limit (UI or chart?).
I tried with CompositionTarget.Rendering
and got the same results: below 20ms there was no difference in speed.
Then I accidentally enabled both and speed doubled. So I tested with multiple threads (2, 3, 4) and speed doubled, tripled and quadrupled. This has no locks yet, as I don't even know what process I need to generate a lock on, but got no data corruption nor memory leaks.
The question I have is why a sluggish chart at 20ms can not run at 10ms but is ridiculously fast when multithreaded? Is the UI refresh process being run faster? Is the chart computation doubled? Or is there a limit to how fast a single DispatcherTimer can be executed?
Thanks!
Edit: I have a background of embedded coding, so when I think of threads and timings, I immediately think of toggling a pin in hardware and hook up a scope to measure process lengths. I am new to threads in C# and there are no pins to hook up scopes. Is there a way to see thread timings graphically?
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(2)
DispatcherTimer 在 UI 线程上触发其 Tick 事件,被认为是低分辨率或低精度计时器,因为它的 Interval 实际上意味着“自上次计时以来的计时不早于 x”。如果 UI 线程正忙于执行任何操作(处理输入、刷新图表等),那么它将延迟计时器的事件。此外,在 UI 线程上以非常低的间隔运行大量 DispatcherTimer 也会降低应用程序的响应速度,因为在引发 Tick 事件时,应用程序无法响应输入。
正如您所指出的,为了频繁处理数据,您应该转移到后台线程。但也有一些警告。您当前没有观察到损坏或其他错误的事实可能纯粹是巧合。如果在前台线程尝试读取列表的同时在后台线程上修改列表,您最终将崩溃(如果幸运的话)或看到损坏的数据。
在您的示例中,您有一条评论说“无需刷新图表,它会自动刷新”。这让我想知道图表如何知道您已更改
datapoints
集合?List
修改时不会引发事件。如果您使用的是ObservableCollection
我会指出,每次删除/添加一个点时,您都可能会刷新图表,这可能会减慢速度。但如果您实际上正在使用
List
那么一定有其他东西(也许是另一个计时器?)正在刷新图表。也许图表控件本身有内置的自动刷新机制?无论如何,这个问题有点棘手 但不是全新的。有多种方法可以在后台线程上维护集合并从 UI 线程绑定到它。但是 UI 刷新速度越快,您等待后台线程释放锁的可能性就越大。
最小化这种情况的一种方法是使用
LinkedList
而不是List
。添加到 LinkedList 末尾的时间复杂度为 O(1),删除一个项目也是如此。当您从开头删除一项时,List
需要将所有内容下移一位。通过使用 LinkedList,您可以在后台线程中锁定它,并且可以最大限度地减少持有锁定的时间。在 UI 线程上,您还需要获取相同的锁,并将列表复制到数组或在持有锁时刷新图表。另一种可能的解决方案是在后台线程上缓冲点的“块”,并使用 Dispatcher.BeginInvoke 将一批数据发布到 UI 线程,然后您可以在其中安全地更新集合。
A DispatcherTimer, which fires its Tick event on the UI thread, is what's considered a low-resolution or low-accuracy timer because its Interval effectively means "tick no sooner than x since the last tick". If the UI thread is busy doing anything (processing input, refreshing the chart, etc.) then it will delay the timer's events. Furthermore, having a bunch of DispatcherTimer's ticking away on the UI thread at very low intervals will also slow down the responsiveness of your application because while the Tick event is being raised, the application can't respond to input.
So as you noted, in order to process data frequently, you should move to a background thread. But there are caveats. The fact that you aren't currently observing corruption or other bugs could be purely coincidental. If the list is being modified on a background thread at the same time the foreground thread is trying to read from it, you will eventually crash (if you're lucky) or see corrupt data.
In your example, you have a comment that says "no need to refresh chart, it does refresh automatically." This makes me wonder how does the chart know that you have changed the
datapoints
collection?List<T>
does not raise events when it is modified. If you were using anObservableCollection<T>
I would point out that each time you remove/add a point you are potentially refreshing the chart, which could be slowing things down.But if you are in fact using
List<T>
then there must be something else (perhaps another timer?) that is refreshing the chart. Maybe the chart control itself has a built-in auto-refresh mechanism?In any event, the problem is a little bit tricky but not completely new. There are ways that you could maintain a collection on a background thread and bind to it from the UI thread. But the faster your UI refreshes, the more likely you'll be waiting for a background thread to release a lock.
One way to minimize this would be to use a
LinkedList<T>
instead ofList<T>
. Adding to the end of a LinkedList is O(1), so is removing an item. AList<T>
needs to shift everything down by one when you remove an item from the beginning. By using LinkedList you can lock on it in the background thread(s) and you'll minimize the amount of time that you're holding the lock. On the UI thread you would also need to obtain the same lock and either copy the list out to an array or refresh the chart while the lock is held.Another possible solution would be to buffer "chunks" of points on the background thread and post a batch of them to the UI thread with Dispatcher.BeginInvoke, where you could then safely update a collection.
我认为这里的关键是要认识到 Silverlight 默认情况下以 60fps 的最大帧速率渲染(可通过 MaxFrameRate 属性进行自定义)。这意味着 DispatcherTimer 每秒最多触发 60 次。此外,所有渲染工作也发生在 UI 线程上,因此 DispatcherTimer 最多以绘图发生的速率触发,如上一张海报所指出的。
通过添加三个计时器所做的结果只是在每个事件循环中触发“添加数据”方法 3 次而不是一次,因此看起来图表运行得更快,但实际上帧速率大致为相同。您可以使用单个 DispatcherTimer 获得相同的效果,只需在每个 Tick 上添加 3 倍的数据即可。您可以通过挂钩 CompositionTarget.Rendering 事件并并行计算帧速率来验证这一点。
之前提出的 ObservableCollection 点是一个很好的点,但是在 Visiblox 中,有一点魔法可以尝试减轻以下影响:因此,如果您以非常快的速度添加数据,图表更新将以渲染循环的速度批量进行,并且将删除不必要的重新渲染。
另外,关于与 IDataSeries 的 ObservableCollection 实现相关联的观点,您可以完全自由地自己实现 IDataSeries 接口,例如通过使用简单的列表来支持它。请注意,显然如果您这样做,当数据发生变化时图表将不再自动更新。您可以通过调用 Chart.Invalidate() 或更改手动设置的轴范围来强制图表更新。
The key here I think is to realise that Silverlight renders at a maximum frame rate of 60fps by default (customisable through your MaxFrameRate property). That means that the DispatcherTimer ticks will fire at most 60 times per second. Additionally, all the rendering work happens on the UI thread as well so the DispatcherTimer fires at the rate that the drawing is happening at best, as pointed out by the previous poster.
The result of what you're doing by adding three timers is just to fire the "add data" method 3 times per event loop rather than once, so it will look like your charts are going much faster but in fact the frame rate is roughly the same. You could get the same effect with a single DispatcherTimer and just add 3 times as much data on each Tick. You can verify this by hooking into the CompositionTarget.Rendering event and counting the frame rate there in parallel.
The ObservableCollection point made previously is a good one but in Visiblox there is a bit of magic to try and mitigate the effects of that so if you're adding data at a very fast rate the chart updates will be batched up at the rate of the render loop and unnecessary re-renders will be dropped.
Also regarding your point about being tied to the ObservableCollection implementation of IDataSeries, you are entirely free to implement the IDataSeries interface yourself, for example by backing it with a simple List. Just be aware that obviously if you do that the chart will no longer automatically update when data changes. You can force a chart update by calling Chart.Invalidate() or by changing a manually set axis range.