简体   繁体   English

在MVVM中,如何防止BackgroundWorker冻结UI?

[英]In MVVM, how can I prevent BackgroundWorker from freezing UI?

First of all, I've seen many similar issues here on SO and around the net. 首先,我在SO和网络上看到了许多类似的问题。 None of these appear to address my particular issue. 这些似乎都无法解决我的特定问题。

I have a simple BackgroundWorker whose job is to read a file line-by-line and report progress to indicate how far through it is. 我有一个简单的BackgroundWorker其工作是逐行读取文件并报告进度以指示通过该文件的距离。 There are up to a total of 65,553 lines in the file, so it is important to me that the BackgroundWorker finishes as fast as possible. 文件中总共有65,553行,因此对我来说重要的是BackgroundWorker尽可能快地完成。

Since MVVM is built on separation of concerns (SoC) and the decoupling of the View and View-Model, the BackgroundWorker updates properties on the View-Model that the View binds to. 由于MVVM建立在关注点分离(SoC)和视图与视图模型的分离的基础上,因此BackgroundWorker更新视图绑定到的视图模型的属性。 My setup is very similar to Kent Boorgaart's answer on another question. 我的设置与Kent Boorgaart在另一个问题上的回答非常相似。

In high-stress scenarios where the BackgroundWorker demands a lot of CPU without sleeping, the UI thread is starved and not able to update any of the bound properties that have been notified via INotifyPropertyChanged . BackgroundWorker需要大量CPU且不休眠的高压力方案中,UI线程处于饥饿状态,无法更新通过INotifyPropertyChanged通知的任何绑定属性。 However, if the BackgroundWorker sleeps, then the job will not finish as fast as possible. 但是,如果BackgroundWorker处于睡眠状态,则作业将无法尽快完成。

How can I ensure the View receives progress updates while respecting MVVM and while not throttling the job? 如何确保View在尊重MVVM的同时又不限制工作的同时接收进度更新?

In the View-Model the BackgroundWorker is setup like this. 在视图模型中, BackgroundWorker的设置如下。 The Start() function is called by a RelayCommand (part of MVVM-Light ). Start()函数由RelayCommandMVVM-Light的一部分Start()调用。

public void Start(string memoryFile)
{
    this.memoryFile = memoryFile;
    BackgroundWorker worker = new BackgroundWorker();
    worker.DoWork += Worker_DoWork;
    worker.ProgressChanged += Worker_ProgressChanged;
    worker.WorkerReportsProgress = true;
    worker.RunWorkerAsync();
}

Here is the code for the actual work performed: 这是执行的实际工作的代码:

private void Worker_DoWork(object sender, DoWorkEventArgs e)
{
    BackgroundWorker bw = (BackgroundWorker)sender;
    IsAnalyzing = true;
    bw.ReportProgress(0, new ProgressState("Processing..."));

    int count = File.ReadLines(memoryFile).Count();
    StreamReader reader = new StreamReader(memoryFile);
    string line = "";
    int lineIndex = 0;
    while ((line = reader.ReadLine()) != null)
    {
        bw.ReportProgress((int)(((double)lineIndex / count) * 100.0d));

        //Process record... (assume time consuming operation)
        HexRecord record = HexFileUtil.ParseLine(line);

        lineIndex++;
        if (lineIndex % 150 == 0)
        {
            //Uncomment to give UI thread some time.
            //However, this will throttle the job.
            //Thread.Sleep(5);
        }
    }
    bw.ReportProgress(100, new ProgressState("Done."));

    Thread.Sleep(1000);
    IsAnalyzing = false;
}

private void Worker_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
    Progress = e.ProgressPercentage;
    if (e.UserState != null)
    {
        Task = ((ProgressState)e.UserState).Task;
    }
}

In the above code, the following properties are used for bindings between the View and View-Model and each will trigger INotifyPropertyChanged.PropertyChanged events: 在上面的代码中,以下属性用于View和View-Model之间的绑定,并且每个属性都会触发INotifyPropertyChanged.PropertyChanged事件:

  • Progress
  • Task
  • IsAnalyzing


EDIT: 编辑:

In follow up Stephen Cleary and Filip Cordas, I've attempted using Task.Run() with and without ObservableProgress . 在跟进Stephen Cleary和Filip Task.Run() ,我尝试使用Task.Run()带有和不带有ObservableProgress

I've simplified the background task to iterate through numbers instead of lines of a file. 我简化了后台任务以遍历数字而不是文件行。

private void DoWork(IProgress<ProgressState> progress)
{
    IsAnalyzing = true;
    progress.Report(new ProgressState(0, "Processing..."));

    for (int i = 0; i < 2000000000; i += 1000000)
    {
        int percent = (int)(((double)i / 2000000000) * 100.0d);
        progress.Report(new ProgressState(percent, String.Format("Processing ({0}%)", percent)));
        Thread.Sleep(5);
    }
    progress.Report(new ProgressState(100, "Done."));

    Thread.Sleep(1000);
    IsAnalyzing = false;
}

Now, I start the task in either one or two ways (with or without the ObservableProgress ): 现在,我以一种或两种方式(有或没有ObservableProgress )启动任务:

public void Start(string memoryFile)
{
    this.memoryFile = memoryFile;

    /* TODO: Uncomment this section to use ObservableProgress instead.
     ObservableProgress.CreateAsync<ProgressState>(progress => System.Threading.Tasks.Task.Run(() => DoWork(progress)))
    .Sample(TimeSpan.FromMilliseconds(50))
    .ObserveOn(Application.Current.Dispatcher)
    .Subscribe(p =>
    {
        Progress = p.ProgressPercentage;
        Task = p.Task;
    });*/

    // TODO: Comment this section to use ObservableProgress instead.
    var progress = new Progress<ProgressState>();
    progress.ProgressChanged += (s, p) =>
    {
        Progress = p.ProgressPercentage;
        Task = p.Task;
    };
    System.Threading.Tasks.Task.Run(() => DoWork(progress));
}

ObservableProgress.cs ObservableProgress.cs

public static class ObservableProgress
{
    public static IObservable<T> CreateAsync<T>(Func<IProgress<T>, Task> action)
    {
        return Observable.Create<T>(async obs =>
        {
            await action(new Progress<T>(obs.OnNext));
            obs.OnCompleted();

            return Disposable.Empty;
        });
    }
}

In both scenarios (with or without ObservableProgress ) I find that I still need to throttle the background job by using Thread.Sleep(5) . 在这两种情况下(无论是否带有ObservableProgress ),我都仍然需要使用Thread.Sleep(5)来限制后台作业。 Otherwise the UI freezes. 否则,UI将冻结。


EDIT 2: 编辑2:

I made a small modification to progress reports inside the worker thread: 我对工作线程中的进度报告做了一些小的修改:

for (int i = 0; i < 2000000000; i += 10) //Notice this loop iterates a lot more.
{
    int percent = (int)(((double)i / 2000000000) * 100.0d);
    //Thread.Sleep(5); //NOT Throttling anymore.
    if (i % 1000000 == 0)
    {
        progress.Report(new ProgressState(percent, String.Format("Processing ({0}%)", percent)));
    }
}

With this modification, the UI does not lock anymore and changes are propagating properly. 通过此修改,UI不再锁定,并且更改正在正确传播。 Why is this so? 为什么会这样呢?

In high-stress scenarios where the BackgroundWorker demands a lot of CPU without sleeping, the UI thread is starved and not able to update any of the bound properties that have been notified via INotifyPropertyChanged. 在BackgroundWorker需要大量CPU且不休眠的高压力方案中,UI线程处于饥饿状态,无法更新通过INotifyPropertyChanged通知的任何绑定属性。 However, if the BackgroundWorker sleeps, then the job will not finish as fast as possible. 但是,如果BackgroundWorker处于睡眠状态,则作业将无法尽快完成。

Having a background thread use CPU won't interfere with the UI thread. 使用后台线程使用CPU不会干扰UI线程。 What I suspect is actually happening is that the background thread is sending progress updates too quickly to the UI thread, and the UI thread is simply unable to keep up. 我怀疑实际上正在发生的事情是,后台线程向UI线程发送进度更新的速度过快 ,而UI线程根本无法跟上进度。 (This ends up looking like a complete "freeze" because of the way Win32 messages are prioritized). (由于Win32消息的优先级排序,最终看起来像是一个完整的“冻结”)。

How can I ensure the View receives progress updates while respecting MVVM and while not throttling the job? 如何确保View在尊重MVVM的同时又不限制工作的同时接收进度更新?

Rather simple: throttle the progress updates . 相当简单:限制进度更新 Or more specifically, sample them. 或更具体地说,对它们进行采样

First, I recommend using Filip's approach of Task.Run with IProgress<T> ; 首先,我建议使用带有IProgress<T> Filip的Task.Run方法; this is the modern equivalent of BackgroundWorker (more info on my blog ). 这相当于BackgroundWorker的现代版本(有关更多信息,请参见我的博客 )。

Second, in order to sample the progress updates, you should use an implementation of IProgress<T> that allows you to sample based on time (ie, don't use Progress<T> ). 其次,为了采样进度更新,您应该使用IProgress<T>的实现,该实现允许您基于时间进行采样(即, 不要使用Progress<T> )。 Asynchronous sequences with time-based logic? 具有基于时间的逻辑的异步序列? Rx is the clear choice. Rx是明确的选择。 Lee Campbell has a great implementation , and I have a lesser one . 李·坎贝尔(Lee Campbell)有很好的执行力 ,而我有一个较小的实现

Example, using Lee Campbell's ObservableProgress : 例如,使用Lee Campbell的ObservableProgress

private void DoWork(IProgress<ProgressState> progress)
{
  IsAnalyzing = true;
  progress.Report(new ProgressState(0, "Processing..."));

  int count = File.ReadLines(memoryFile).Count();
  StreamReader reader = new StreamReader(memoryFile);
  string line = "";
  int lineIndex = 0;
  while ((line = reader.ReadLine()) != null)
  {
    progress.Report(new ProgressState((int)(((double)lineIndex / count) * 100.0d));

    //Process record... (assume time consuming operation)
    HexRecord record = HexFileUtil.ParseLine(line);

    lineIndex++;
  }
  progress.Report(new ProgressState(100, "Done."));
  IsAnalyzing = false;
}

...

ObservableProgress.CreateAsync<ProgressState>(progress => Task.Run(() => DoWork(progress)))
    .Sample(TimeSpan.FromMilliseconds(250)) // Update UI every 250ms
    .ObserveOn(this) // Apply progress updates on UI thread
    .Subscribe(p =>
    {
      Progress = p.ProgressPercentage;
      Task = p.Task;
    });

Why are you using BackgroundWorker? 为什么要使用BackgroundWorker? Here is a simple progress implementation with tasks and it won't block UI thread if you access PropertyChanged invoke 这是一个带有任务的简单进度实现,如果您访问PropertyChanged调用,它不会阻止UI线程

Do = new GalaSoft.MvvmLight.Command.RelayCommand(()=>
            {
                var progress = new Progress<int>();
                progress.ProgressChanged += (s, p) => Progress = p;
                //Run and forget 
                DoWork(progress);
            });
public async Task DoWork(IProgress<int> progress = null)
        {
            await Task.Run(() =>
            {
                for (int i = 1; i < 11; i++)
                {
                    var count = 0;
                    for (int j = 0; j < 10000000; j++)
                    {
                        count += j;
                    }
                    progress.Report(i * 10);
                }
            });
        }

Some more info on the subject https://blogs.msdn.microsoft.com/dotnet/2012/06/06/async-in-4-5-enabling-progress-and-cancellation-in-async-apis/ For async programing 有关此主题的更多信息https://blogs.msdn.microsoft.com/dotnet/2012/06/06/async-in-4-5-enabling-progress-and-cancellation-in-async-apis/对于异步程式设计

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM