【问题标题】:INotifyPropertyChanged boggs down when handling a lot of dataINotifyPropertyChanged 在处理大量数据时陷入困境
【发布时间】:2016-10-27 11:57:13
【问题描述】:

我有一个 HID 设备,我正在以大约 200hz-600hz 的频率与之通信,并将数据解释为代表 HID 设备属性的类对象。该类在其属性上实现了 INotifyPropertyChanged,由于通信速度的原因,我认为处理队列正在陷入困境,因为几分钟后控件似乎变得迟缓和“框架”。

.net 中是否存在可能有助于解决此类问题的方法,比如事件处理程序池或某种队列?

不幸的是,没有我的 HID 设备,我不确定我的代码是否对任何人都可以复制,但我将包含几个相关的 sn-ps 来展示我的实现:

public enum DataEvents { onNone = 0, onStatus = 1, onInput = 2, onOutput = 4, onReport = 8};
public class Controller: INotifyPropertyChanged, IDisposable, INotifyDisposed
{
    public event PropertyChangedEventHandler PropertyChanged;
    public event EventHandler Disposing;
    public event EventHandler Disposed;
    public event EventHandler ReportReceived;

    internal void callPropertyChanged(string PropertyName)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(PropertyName));
    }

    internal void callReportReceived()
    {
        ReportReceived?.Invoke(this, EventArgs.Empty);
    }

    public bool Touch1
    {
        get { return _Touch1; }
        private set { if (value != _Touch1) { _Touch1 = value; if (RaiseUpdateEvents.HasFlag(DataEvents.onInput)) callPropertyChanged("Touch1"); } }
    }
    private bool _Touch1 = false;

    //There are many more properties but all of them follow this pattern, and have several different types
}

我的对象是从一个循环中的System.Threading.Thread 填充的,该循环轮询 HID 设备以获取报告,HID 设备读取方法是一个阻塞调用,因此该循环不是死循环,并且受限于设备的数据速率,如通常所说的 200hz-600hz。

编辑:值得注意的是,我对 WPF 绑定特别感兴趣。

【问题讨论】:

  • 如果这是 Wpf,您将导致调度程序积压。减少活动的频率
  • @Gusdor 好吧,当 HID 设备属性更改时会引发该事件,那么我将如何缓解这种情况
  • @Gusdor 我正在考虑为每个属性设置一个队列,如果为一个属性引发多个事件,它将清除除最新事件之外的所有事件,但我看不出这不会增加延迟和延迟,尤其是在这些速度。
  • 与其直接在由 HID 更新的对象上使用 INotifyPropertyChanged,不如设置一个合理速率的 DispatcherTimer(可能每 100 毫秒左右滴答一次)。当它滴答作响时,读取对象的属性,并使用它来更新将绑定到 UI 的另一个对象。
  • @Wobbles, unmanaged 在 .Net 生态系统的上下文中意味着您正在使用在 CLR 之外执行的本机库。为 .Net 编译的任何 .Net 库都是托管的。在这种情况下使用这个词来描述其他任何东西都是令人困惑的。请不要。

标签: c# wpf inotifypropertychanged


【解决方案1】:

在 WPF 中处理近乎实时的系统时(我在过去 6 年中一直致力于这些系统),您有几个选择。首先,我将列出一些值得深思的地方:

  • 要通过一个事件更新所有 WPF 绑定,请使用 string.Empty 作为您的属性名称。
  • 您的问题可能不完全是由事件引起的。 WPF 有很多影响内存管理的问题。

所以你要问的问题是用户需要多久看到一次任何类型的变化?人类的视觉持久性是 1/10 秒,即 100 毫秒。任何比这更频繁的更新都是浪费的,但更常见的是,即使是太频繁了。

每秒一个事件?

在我的场景中,我们确定我们只需要每秒更新一次屏幕上的所有内容。尽管我们每秒最多接收 12 次数据(每个样本 83 毫秒),但我们还是收集并平均了数据以使其平滑。它让我们的用户更好地了解正在发生的事情。

  • 我们将视图模型构建为使用主计时器每秒调用一次 Update() 方法。
  • 模型实现了INotifyPropertyChanged 以避免binding memory leak,但仅引发了string.Empty 的一个属性更改事件以导致UI 刷新

最小化对象创建

在垃圾回收中花费的每一毫秒都是用户无法与您的应用程序交互的明显时间量。每次引发事件时,都必须创建要发送的事件对象。虽然从技术上讲,您可以创建一次事件对象并引发相同的实例,但 WPF 在多个地方为您创建对象实例。这些是您需要注意的事项:

  • DataTemplate 实质上为您要模板化的每个对象创建一个新模板。尽可能尝试使用虚拟化,并尽量减少使用。
  • ResourceDictionary 每次您在控件中声明资源字典时,您都在创建一个新实例。最好将所有合并的资源字典放在App.xaml 中,而不是在不同的用户控件中包含相同的字典。特别是如果您在 DataTemplate 中有用户控件
  • ContentPresenter 不是你的朋友。

为了进一步解释,ContentPresenter 将获取您的对象,在控件的 ResourceDictionary 中查找它的类型以找到 DataTemplate 以实例化您的数据。当您需要将窗口的特定部分换成另一个控件时,它会很方便,但它确实需要付出巨大的代价。尽可能减少它的使用。

将硬件/通信保留在后台

我们专门设置线程来处理通信和处理需求。这可以让 UI 保持响应,同时我们可以对数据进行一些 DSP/统计缩减。

使用内存分析器

当您需要在显示器上进行近乎实时的更新时,您必须特别小心内存使用。这是我们必须解决的第一个问题。

  • 您的应用程序可以正常启动,但一两分钟后它开始降级
  • 确保您没有持有意外的对象实例
  • 寻找能够在垃圾回收事件中幸存的对象

【讨论】:

    【解决方案2】:

    让我们做一些简单的事情。无论哪种方式,您都需要下采样您的数据。在 UI 上以 200hz-600hz 显示数据通常会导致问题。

    我的建议 - 以您可以接受的最长持续时间启动您选择的计时器。让我们从 1000 毫秒开始。

    1. 每次计时器到期时,为您的所有用户触发该事件 属性更改事件
    2. 不要在设备更新时引发属性更改事件

    每秒一次,您的处理将更新,您的应用程序将保持响应。

    【讨论】:

    • 对于硬件交互,即使是100ms的定时器也是不可接受的。我认为我的关键是某种队列,如果某个属性的事件确实开始堆积,它只需要最近的值。
    • @Wobbles 100ms 应该没问题。 20ms也应该很好用。重要的是您没有尽可能快地运行,因为这总是会在某处造成瓶颈。我以 1 秒为例,因为它在 UI 和您可能输出的任何调试日志中都非常明显。
    • 这种规模的“硬件交互”不应与 GUI 耦合。
    【解决方案3】:

    这里讨论了很多很好的信息,但就实施而言,这是我的做法:

        private Thread PropertyChangedQueueThread;
        private List<string> ChangedPropertiesQueue = new List<string>();
        private ManualResetEvent PropertyChangedQueueBlocker = new ManualResetEvent(false);
    
        private void PropertyChangedQueueWorker()
        {
            while (!this.disposingValue)
            {
                PropertyChangedQueueBlocker.WaitOne();
    
                string PropName = ChangedPropertiesQueue.Last();
                ChangedPropertiesQueue.RemoveAll(i => i == PropName);
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(PropName));
    
                if (ChangedPropertiesQueue.Count() == 0)
                    PropertyChangedQueueBlocker.Reset();
            }
        }
    
        internal void QueuePropertyChangedEvent(string PropertyName)
        {
            ChangedPropertiesQueue.Add(PropertyName);
            PropertyChangedQueueBlocker.Set();
        }
    

    似乎工作得很好,而且它似乎可以自我扩展,所以无论有多少属性更改进入,只有最近的一次会显示到 UI。除非我想象性能会提高,否则它似乎可以很好地满足我的需求。

    我不太确定使用 List 和 .RemoveAll 会造成什么样的性能损失,因为我理解的每次调用 RemoveAll 都会重新索引列表。也许对可以是字符串的键进行索引的类型更适合,例如 Dictionary

    【讨论】:

    • 您可以通过仅发送 string.Empty 作为您的属性名称来降低复杂性。这避免了由于属性名称恰好被掩埋和丢弃而导致部分 UI 不更新的潜在陷阱。
    • @BerinLoritsch 你的意思是在最后的事件调用中?
    • @BerinLoritsch 执行此操作时性能下降,怀疑有太多属性,并且并非所有属性都在不断变化,以便全面更新以具有任何性能价值。
    • 是的,用于最终事件调用。您的 PropertyChangedQueueBlocker 正在执行速率限制,这是主要的。队列本身 (List&lt;string&gt; ChangedPropertiesQueue) 可以替换为简单的bool RaisedPropertyChanged。这可以防止在没有任何更改时发送属性更改。如果您在所有这些都已准备就绪的情况下说性能坦克,那是不对的。不应该以一种或另一种方式改变性能。
    • ..." performance change one way or other" ... 意思是说三项赛的表现应该是相似的。这里的关键是限制更新的速率。
    猜你喜欢
    • 2022-01-07
    • 1970-01-01
    • 2013-06-01
    • 1970-01-01
    • 1970-01-01
    • 2012-07-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多