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

首先,我在SO和网络上看到过很多类似的问题。 这些似乎都没有解决我的特定问题。

我有一个简单的BackgroundWorker其工作是逐行读取文件并报告进度以指示它有多远。 文件中总共有65,553行,因此对我来说, BackgroundWorker尽可能快地完成是很重要的。

由于MVVM建立在关注点分离(SoC)和View和View模型的分离之上,因此BackgroundWorker更新View绑定到的View-Model上的属性。 我的设置与Kent Boorgaart在另一个问题上的答案非常相似。

在高压力情况下, BackgroundWorker需要大量CPU而不会hibernate,UI线程缺乏并且无法更新已通过INotifyPropertyChanged通知的任何绑定属性。 但是,如果BackgroundWorkerhibernate,则作业将无法尽快完成。

如何在尊重MVVM的同时确保View接收进度更新,同时不限制作业?

在View-Model中, BackgroundWorker设置如下。 Start()函数由RelayCommand ( MVVM-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(); } 

以下是实际工作的代码:

 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; } } 

在上面的代码中,以下属性用于View和View-Model之间的绑定,每个属性都会触发INotifyPropertyChanged.PropertyChanged事件:

  • Progress
  • Task
  • IsAnalyzing

编辑:

在后续的Stephen Cleary和Filip Task.Run() ,我尝试使用带有和不带ObservableProgress Task.Run()

我已经简化了后台任务来迭代数字而不是文件的行。

 private void DoWork(IProgress 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; } 

现在,我以一种或两种方式启动任务(使用或不使用ObservableProgress ):

 public void Start(string memoryFile) { this.memoryFile = memoryFile; /* TODO: Uncomment this section to use ObservableProgress instead. ObservableProgress.CreateAsync(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(); progress.ProgressChanged += (s, p) => { Progress = p.ProgressPercentage; Task = p.Task; }; System.Threading.Tasks.Task.Run(() => DoWork(progress)); } 

ObservableProgress.cs

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

在两种情况下(有或没有ObservableProgress )我发现我仍然需要使用Thread.Sleep(5)来限制后台作业。 否则UI会冻结。


编辑2:

我对工作线程内部的报告进行了一些小修改:

 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))); } } 

通过此修改,UI不再锁定,并且更改正在正常传播。 为什么会这样?

在高压力情况下,BackgroundWorker需要大量CPU而不会hibernate,UI线程缺乏并且无法更新已通过INotifyPropertyChanged通知的任何绑定属性。 但是,如果BackgroundWorkerhibernate,则作业将无法尽快完成。

拥有后台线程使用CPU不会干扰UI线程。 我怀疑实际发生的是后台线程向UI线程发送进度更新太快 ,而UI线程根本无法跟上。 (由于Win32消息的优先级排序方式,这最终看起来像一个完整的“冻结”)。

如何在尊重MVVM的同时确保View接收进度更新,同时不限制作业?

相当简单:限制进度更新 。 或者更具体地说,对它们进

首先,我建议使用Filip的Task.Run方法和IProgress ; 这是BackgroundWorker的现代版本( 我的博客上有更多信息)。

其次,为了对进度更新进行采样,您应该使用IProgress的实现,它允许您基于时间进行采样(即, 使用Progress )。 具有基于时间的逻辑的异步序列? Rx是明智的选择。 李坎贝尔有一个很好的实施 ,我有一个较小的 。

例如,使用Lee Campbell的ObservableProgress

 private void DoWork(IProgress 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(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; }); 

你为什么使用BackgroundWorker? 这是一个包含任务的简单进度实现,如果访问PropertyChanged调用,它将不会阻止UI线程

 Do = new GalaSoft.MvvmLight.Command.RelayCommand(()=> { var progress = new Progress(); progress.ProgressChanged += (s, p) => Progress = p; //Run and forget DoWork(progress); }); public async Task DoWork(IProgress 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); } }); } 

关于这个主题的更多信息https://blogs.msdn.microsoft.com/dotnet/2012/06/06/async-in-4-5-enabling-progress-and-cancellation-in-async-apis/用于异步程序设计