【问题标题】:Fast changing collection MVVM WPF - high CPU usage & UI almost freezes快速变化的集合 MVVM WPF - 高 CPU 使用率和 UI 几乎冻结
【发布时间】:2018-04-26 01:25:02
【问题描述】:

我正在开发一个应用程序,其数据网格显示某些正在运行的 Windows 进程(在我的示例 Chrome 进程中)。 选中复选框时,数据网格会加载进程。

要求:

  • 显示每个进程的名称、内存使用情况(私有工作集)的“实时”信息,就像在 Windows 任务管理器 - 进程选项卡中一样。
  • 监控退出并从数据网格中删除它们的进程。
  • 监控某些启动的进程。

使用过的技术:

问题:

  • 加载进程时,CPU 使用率变得非常高,用户界面几乎死机。
  • 即使调用了ManagerService.Stop(),CPU 使用率仍然很高。
  • 从集合中删除进程时,有时会引发 System.InvalidOperationException - Cannot change ObservableCollection during a CollectionChanged event 异常。

我该如何解决这个问题?我的方法也是一种“良好做法”吗?

任何帮助将不胜感激!我已经在这个问题上花费了很多时间。

更新 1

没有帮助,删除 OnRendering() 并实施 INotifyPropertyChanged

public class CustomProcess : INotifyPropertyChanged
{
    private double _memory;

    public double Memory
    {
        get { return _memory; }
        set
        {
            if (_memory != value)
            {
                _memory = value;
                OnPropertyChanged(nameof(Memory));
            }
        }
    }


    private bool _isChecked;

    public bool IsChecked
    {
        get { return _isChecked; }
        set
        {
            if (_isChecked != value)
            {
                _isChecked = value;
                OnPropertyChanged(nameof(IsChecked));
    }
}

更新 2

按照我更新的 Evk 建议

  • 使用常规 ObservableCollection
  • 将计时器移至视图模型

CPU 使用率现在要低得多。 但是我有时会在OnProcessStarted() 中遇到Process with an ID of ... is not running 异常

视图模型

public class MainViewModel 
    {
        System.Threading.Timer timer;
        private ObservableCollection<CustomProcess> _processes;
        public ObservableCollection<CustomProcess> Processes
        {
            get
            {
                if (_processes == null)
                    _processes = new ObservableCollection<CustomProcess>();

                return _processes;
            }
        }
        private void OnBooleanChanged(PropertyChangedMessage<bool> propChangedMessage)
        {
            if (propChangedMessage.NewValue == true)
            {
                _managerService.Start(_processes);
                timer = new System.Threading.Timer(OnTimerTick, null, 0, 200); //every 200ms
                ProcessesIsVisible = true;
            }
            else
            {
                timer.Dispose();
                _managerService.Stop();
                ProcessesIsVisible = false;
            }
        }
        private void OnTimerTick(object state)
        {
            try
            {
                for (int i = 0; i < Processes.Count; i++)
                    Processes[i].UpdateMemory();
            }
            catch (Exception)
            {

            }
        }

型号

public class CustomProcess : INotifyPropertyChanged
    {    
        public void UpdateMemory()
        {
            if (!ProcessObject.HasExited)
                Memory = Process.GetProcessById(ProcessObject.Id).PagedMemorySize64;
        }
        private double _memory;

        public double Memory
        {
            get { return _memory; }
            set
            {
                if (_memory != value)
                {
                    _memory = value;
                    OnPropertyChanged(nameof(Memory));
                }
            }
        }

服务

        private void OnProcessNotification(NotificationMessage<Process> notMessage)
        {
            if (notMessage.Notification == "exited")
            {
                _processes.Remove(p => p.ProcessObject.Id == notMessage.Content.Id, DispatcherHelper.UIDispatcher);
            }

        }

原码

XAML

<DataGrid ItemsSource="{Binding Processes}">
   <DataGridTextColumn Header="Process name"
                            Binding="{Binding ProcessObject.ProcessName}"
                            IsReadOnly='True'
                            Width='Auto' />
        <DataGridTextColumn Header="PID"
                            Binding="{Binding ProcessObject.Id}"
                            IsReadOnly='True'
                            Width='Auto' />
        <DataGridTextColumn Header="Memory"
                            Binding='{Binding Memory}'
                            IsReadOnly='True'
                            Width='Auto' />
</DataGrid>

XAML 代码隐藏

public MainWindow()
{
        InitializeComponent();
        DataContext = SimpleIoc.Default.GetInstance<MainViewModel>();
        CompositionTarget.Rendering += OnRendering;
    }

    private void OnRendering(object sender, EventArgs e)
    {
        if (DataContext is IRefresh)
            ((IRefresh)DataContext).Refresh();
    }
}

视图模型

public class MainViewModel : Shared.ViewModelBase, IRefresh
{
    private AsyncObservableCollection<CustomProcess> _processes;
    public AsyncObservableCollection<CustomProcess> Processes
    {
        get
        {
            if (_processes == null)
                _processes = new AsyncObservableCollection<CustomProcess>();

            return _processes;
        }
    }
    private readonly IManagerService _managerService;

    public MainViewModel(IManagerService managerService)
    {
        _managerService = managerService;
        Messenger.Default.Register<PropertyChangedMessage<bool>>(this, OnBooleanChanged);
    }      

    #region PropertyChangedMessage
    private void OnBooleanChanged(PropertyChangedMessage<bool> propChangedMessage)
    {
        if (propChangedMessage.NewValue == true)
        {
            _managerService.Start(_processes);
        }
        else
        {
            _managerService.Stop();
        }
    }

    public void Refresh()
    {
        foreach (var process in Processes)
            RaisePropertyChanged(nameof(process.Memory)); //notify UI that the property has changed
    }

服务

public class ManagerService : IManagerService
{
    AsyncObservableCollection<CustomProcess> _processes;
    ManagementEventWatcher managementEventWatcher;

    public ManagerService()
    {
        Messenger.Default.Register<NotificationMessage<Process>>(this, OnProcessNotification);
    }

    private void OnProcessNotification(NotificationMessage<Process> notMessage)
    {
        if (notMessage.Notification == "exited")
        {
            //a process has exited. Remove it from the collection
            _processes.Remove(p => p.ProcessObject.Id == notMessage.Content.Id);
        }

    }

    /// <summary>
    /// Starts the manager. Add processes and monitor for starting processes
    /// </summary>
    /// <param name="processes"></param>
    public void Start(AsyncObservableCollection<CustomProcess> processes)
    {
        _processes = processes;
        _processes.CollectionChanged += OnCollectionChanged;

        foreach (var process in Process.GetProcesses().Where(p => p.ProcessName.Contains("chrome")))
            _processes.Add(new CustomProcess(process));

        MonitorStartedProcess();
        Task.Factory.StartNew(() => MonitorLogFile());
    }

    /// <summary>
    /// Stops the manager.
    /// </summary>
    public void Stop()
    {       
        _processes.CollectionChanged -= OnCollectionChanged;
        managementEventWatcher = null;
        _processes = null;
    }

    private void MonitorLogFile()
    {
        //this code monitors a log file for changes. It is possible that the IsChecked property of a CustomProcess object is set in the Processes collection
    }

    /// <summary>
    /// Monitor for started Chrome
    /// </summary>
    private void MonitorStartedProcess()
    {
        var qStart = "SELECT * FROM Win32_ProcessStartTrace WHERE ProcessName like '%chrome%'";
        ManagementEventWatcher managementEventWatcher = new ManagementEventWatcher(new WqlEventQuery(qStart));
        managementEventWatcher.EventArrived += new EventArrivedEventHandler(OnProcessStarted);
        try
        {
            managementEventWatcher.Start();
        }
        catch (Exception)
        {

        }
    }



    private void OnProcessStarted(object sender, EventArrivedEventArgs e)
    {

        try
        {
            int pid = Convert.ToInt32(e.NewEvent.Properties["ProcessID"].Value);
            _processes.Add(new CustomProcess(Process.GetProcessById(pid)));  //add to collection
        }
        catch (Exception)
        {

        }

    }

型号

public class CustomProcess
{        
    public Process ProcessObject { get; }

    public CustomProcess(Process process)
    {
        ProcessObject = process;
        try
        {
            ProcessObject.EnableRaisingEvents = true;
            ProcessObject.Exited += ProcessObject_Exited;
            Task.Factory.StartNew(() => UpdateMemory());
        }
        catch (Exception)
        {

        }

    }

    private void ProcessObject_Exited(object sender, EventArgs e)
    {
        Process process = sender as Process;
        NotificationMessage<Process> notMessage = new NotificationMessage<Process>(process, "exited");
        Messenger.Default.Send(notMessage); //send a notification that the process has exited
    }

    private void UpdateMemory()
    {
        while (!ProcessObject.HasExited)
        {
            try
            {
                Memory = Process.GetProcessById(ProcessObject.Id).PagedMemorySize64;
            }
            catch (Exception)
            {

            }
        }
    }

    private double _memory;

    public double Memory
    {
        get { return _memory; }
        set
        {
            if (_memory != value)
            {
                _memory = value;
            }
        }
    }


    private bool _isChecked;

    public bool IsChecked
    {
        get { return _isChecked; }
        set
        {
            if (_isChecked != value)
            {
                _isChecked = value;
            }
        }
    }

【问题讨论】:

  • 我相信你不需要 OnRendering 在代码后面的方法和 Refresh 在 ViewModel 中。您应该使用 WPF 通知系统 --- 要实现这一点,您应该在 CustomProcess 类中实现 INotifyProerptyChanged。因此,如果源值发生变化,WPF 引擎本身会刷新绑定的列值。
  • 没有更改通知的好方法。没有它,就无法正确实现 MVVM。
  • @user1672994 谢谢,我已经尝试过了(实现了 INotifyPropertyChanged,删除了 OnRendering()),但效果相同,CPU 使用率高并且几乎冻结 UI。
  • 这里有几个问题,但主要问题是:您真的需要经常更新流程统计信息吗?说每 200 毫秒还不够好吗?
  • @Evk 200 毫秒就足够了,确实

标签: c# .net wpf multithreading mvvm


【解决方案1】:

写入 GUI 的成本很高。如果您只为每个用户触发的事件执行一次,您将不会注意到它。但是一旦你从任何类型的循环中编写——包括在另一个线程上运行的循环——你就会注意到它。我什至为 Windows 窗体编写了一些示例代码来展示这一点:

using System;
using System.Windows.Forms;

namespace UIWriteOverhead
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        int[] getNumbers(int upperLimit)
        {
            int[] ReturnValue = new int[upperLimit];

            for (int i = 0; i < ReturnValue.Length; i++)
                ReturnValue[i] = i;

            return ReturnValue;
        }

        void printWithBuffer(int[] Values)
        {
            textBox1.Text = "";
            string buffer = "";

            foreach (int Number in Values)
                buffer += Number.ToString() + Environment.NewLine;
            textBox1.Text = buffer;
        }

        void printDirectly(int[] Values){
            textBox1.Text = "";

            foreach (int Number in Values)
                textBox1.Text += Number.ToString() + Environment.NewLine;
        }

        private void btnPrintBuffer_Click(object sender, EventArgs e)
        {
            MessageBox.Show("Generating Numbers");
            int[] temp = getNumbers(10000);
            MessageBox.Show("Printing with buffer");
            printWithBuffer(temp);
            MessageBox.Show("Printing done");
        }

        private void btnPrintDirect_Click(object sender, EventArgs e)
        {
            MessageBox.Show("Generating Numbers");
            int[] temp = getNumbers(1000);
            MessageBox.Show("Printing directly");
            printDirectly(temp);
            MessageBox.Show("Printing done");
        }
    }
}

您的代码甚至更糟,因为您允许更新和布局代码在每次更新之间运行。虽然它确实保持 UI 响应,但它需要运行更多代码。

您将无法绕过限制更新。我会把这些限制清楚地放在视图端。我个人更喜欢这种方式:

  1. 不要将更改通知事件注册到 Observable 集合中
  2. 制作一个计时器,定期使用 Collection 的当前值更新 UI。将计时器设置为每秒 60 次更新。这对人类来说应该足够快了。
  3. 您可能希望在编写集合和访问器代码的代码中添加某种形式的锁定以避免竞争条件。

一些旁注:

我的一个小毛病是 Exception Hanlding。我在那里看到了一些致命异常的吞咽。你真的应该尽快解决这个问题。线程可以意外吞下异常已经够糟糕的了,你不应该为此编写额外的代码。这是我经常链接的两篇文章:http://blogs.msdn.com/b/ericlippert/archive/2008/09/10/vexing-exceptions.aspx | http://www.codeproject.com/Articles/9538/Exception-Handling-Best-Practices-in-NET

其次,ObservableColelctions 在完全返工时是出了名的糟糕。它缺少一个 add-range 函数。因此,每一次更改都会触发更新。我通常的解决方法是: 1. 给属性暴露Collection Change Notification 2. 不要在任何更新上使用暴露的集合。 3. 而是使用背景集合。只有当这个新状态完成后,你才会暴露它。

【讨论】:

  • 感谢 Christopher,请参阅“更新 2”,但我不明白您的“常用解决方法”。您能给我一些实用的提示吗?例如新状态完成后如何公开后台集合。您是否为整个集合调用 OnPropertyChanged()?
【解决方案2】:

您无需自己更新/刷新 UI,而是使用 WPF 更改通知系统,该系统通过 DataBindingPropertyChanged 事件实现。

正如 MSDN 引用的那样 -

INotifyPropertyChanged 接口用于通知客户端(通常是绑定客户端)属性值已更改。

例如,考虑一个具有名为FirstName 的属性的Person 对象。为了提供通用的属性更改通知,Person 类型实现了INotifyPropertyChanged 接口,并在FirstName 更改时引发PropertyChanged 事件。

更多详情here.

【讨论】:

  • 谢谢,我已经尝试过了(实现 INotifyPropertyChanged,删除了 OnRendering()),但效果相同,CPU 使用率高并且几乎冻结 UI
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-01-16
  • 1970-01-01
  • 2019-05-07
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多