在MVVM中,如何防止BackgroundWorker冻结UI?
首先,我在SO和网络上看到过很多类似的问题。 这些似乎都没有解决我的特定问题。
我有一个简单的BackgroundWorker
其工作是逐行读取文件并报告进度以指示它有多远。 文件中总共有65,553行,因此对我来说, BackgroundWorker
尽可能快地完成是很重要的。
由于MVVM建立在关注点分离(SoC)和View和View模型的分离之上,因此BackgroundWorker
更新View绑定到的View-Model上的属性。 我的设置与Kent Boorgaart在另一个问题上的答案非常相似。
在高压力情况下, BackgroundWorker
需要大量CPU而不会hibernate,UI线程缺乏并且无法更新已通过INotifyPropertyChanged
通知的任何绑定属性。 但是,如果BackgroundWorker
hibernate,则作业将无法尽快完成。
如何在尊重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/用于异步程序设计