使用后台线程让应用程序平稳运行确实会产生很大的不同。将非必要进程卸载到一个或多个后台线程有助于保持应用程序的 UI 响应迅速。
但是,与单线程程序相比,多线程程序需要更多的精力来开发。 不当的线程使用是导致细微错误的一个重要原因,这可能需要花费大量时间来定位。
本文旨在帮助理解多线程图表应用程序并解释 .NET 多线程的一些概念,特别是应如何在多线程程序中使用 LightningChart .NET 控件。
下图是例子:
Visual Studio 中多线程应用程序的 Parallel Stacks 窗口示例
大多数操作系统对涉及用户界面的代码使用单线程模型。 该模型对于正确排序用户界面事件(包括击键和触摸输入)是必要的。
该线程通常称为主线程、用户界面线程或 UI 线程。 严格来说,.NET 对前端和后端的分类是不同的。
但在本文中,我们将把非 UI 线程称为后台线程。
多线程
了解多线程图表应用程序可以提高应用程序的性能,但对 UI 控件的访问本身并不是线程安全的。
多线程可以将您的代码暴露给严重和复杂的错误。 操作控件的两个或多个线程可能会强制控件进入不一致的状态并导致竞争条件、访问冲突、锁死以及冻结或挂起。如果您在应用程序中实现多线程,请确保您以线程安全的方式调用跨线程控件。
UI 库(Windows 窗体、WPF 和 UWP)的设计方式是,只能从 UI 线程访问 UI 元素。 Windows 这样做的目的是是为了确保 UI 控件的完整性。LightningChart® .NET 控件是一个 UI 元素。 因此,适用相同的线程安全规则。 与其他所有 UI 控件一样,它要求所有 LightningChart 属性都应在 UI 线程中更新。
在应用程序中使用后台线程时,来自线程的所有 UI 更新都必须通过 Invoke(WinForms 中的 Control.Invoke() 和 WPF 中的 Dispatcher.Invoke())。 这两种方法都安排一个委托执行。
WPF 调度程序更加灵活,因为它允许用户在向调度程序队列添加元素时指定队列优先级。
多线程应用Demo
这种多线程应用程序设计在我们的demo中的ExampleThreadMultiChannel 中进行了演示。
我们经常使用这种设计,因为它非常适合在多个线程上分配计算以及尽可能快地更新图表。
这个想法是有一个后台线程来读取/收集/生成新数据并通过 Invoke() 调用推送一些“新点”。
Demo显示了线程安全图表更新和数据生成的后台线程使用情况。
LightningChart 渲染的原理很简单。 每当用户添加数据(或修改属性)时,都会呈现图表。
该过程涉及大量处理,包括并行循环计算、对象处置、相关属性的更新、对象的重新创建等。
出于这个原因,只有一个线程应该更新和读取图表——串行/同步执行。
Headless Demo Service
下面是客户端对消息的内部操作的示意图,其中显示了命名管道和后台线程。
对于标准的桌面应用程序,LightningChart 线程与主 UI 线程相同。 但是,该线程不一定是主 UI 线程。
Headless 模式(用户手册第 24 章,图 3)允许图表在后台线程中运行,但所有更新都应通过创建图表的同一线程。
另一个用例是,例如,当应用程序可能有多个窗口,并且每个窗口可能有自己的线程时。 对于这种情况,LightningChart 被允许使用它添加到的窗口的线程(这可能是与在主应用程序窗口上运行的线程不同的线程)。
这种设计在我们的测试对比中实现,如下图所示:
多窗口 - 多线程方法
值得注意的是,Dispatcher/Control 有两个类似的方法:Invoke() 和 BeginInvoke()。 这两种方法都安排一个委托执行。
- Invoke 是一个同步调用——也就是说,它在 UI 线程真正完成执行委托之前不会返回。
- BeginInvoke 是异步的并立即返回。
另一方面(在 LightningChart .NET 的情况下)通过 BeginInvoke 更新图表,用户立即获得响应式 UI。
然而,另一方面,当实际渲染发生时,应用程序的信息有限。 通过更改图表更新类型可以实现类似的效果:
_chart.(Chart)RenderOptions.UpdateType = ChartUpdateTypes.Async;
其它事项
System.Threading.Tasks 类是一个特例,包括流行的 Parallel.Fora 任务对象,它通常在线程池线程上异步执行,而不是在主应用程序线程上同步执行。
因此,该任务更有可能在后台线程中执行而不是在 UI 线程中执行。 如果进程打算在 UI 线程上运行,这可能会导致线程中的访问冲突。
如果您不将微小的图表更新安排到更大的批次中,多线程图表应用方法并不会提高性能。
如果同时以编程方式更改了多个属性,则应在 BeginUpdate() 和 EndUpdate() 方法调用之间作为批处理进行属性更改(用户手册第 15.3 章)。
否则,每次属性更新都会刷新图表并导致大量额外消耗。
每当您的应用程序中有大型且耗时的项目时,请考虑使用后台线程/多个线程。但是,不要忘记 LightningChart 是一个 UI 元素。 因此,为了线程安全,应在 UI 线程中更新所有LightningChart 属性.
延展阅读
- Use the Parallel Stacks window (Microsoft Visual Studio documentation)
- Foreground and background threads (Microsoft .NET documentation)
- How to make thread-safe calls to controls (Windows Forms .NET)
- Threading Model (Microsoft .NET documentation)