[英]How can I prevent the backgroundWorker causing the UI to become sluggish?
[英]In MVVM, how can I prevent BackgroundWorker from freezing UI?
首先,我在SO和网络上看到了许多类似的问题。 这些似乎都无法解决我的特定问题。
我有一个简单的BackgroundWorker
其工作是逐行读取文件并报告进度以指示通过该文件的距离。 文件中总共有65,553行,因此对我来说重要的是BackgroundWorker
尽可能快地完成。
由于MVVM建立在关注点分离(SoC)和视图与视图模型的分离的基础上,因此BackgroundWorker
更新视图绑定到的视图模型的属性。 我的设置与Kent Boorgaart在另一个问题上的回答非常相似。
在BackgroundWorker
需要大量CPU且不休眠的高压力方案中,UI线程处于饥饿状态,无法更新通过INotifyPropertyChanged
通知的任何绑定属性。 但是,如果BackgroundWorker
处于睡眠状态,则作业将无法尽快完成。
在视图模型中, 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()
,我尝试使用Task.Run()
带有和不带有ObservableProgress
。
我简化了后台任务以遍历数字而不是文件行。
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;
}
现在,我以一种或两种方式(有或没有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
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;
});
}
}
在这两种情况下(无论是否带有ObservableProgress
),我都仍然需要使用Thread.Sleep(5)
来限制后台作业。 否则,UI将冻结。
我对工作线程中的进度报告做了一些小的修改:
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且不休眠的高压力方案中,UI线程处于饥饿状态,无法更新通过INotifyPropertyChanged通知的任何绑定属性。 但是,如果BackgroundWorker处于睡眠状态,则作业将无法尽快完成。
使用后台线程使用CPU不会干扰UI线程。 我怀疑实际上正在发生的事情是,后台线程向UI线程发送进度更新的速度过快 ,而UI线程根本无法跟上进度。 (由于Win32消息的优先级排序,最终看起来像是一个完整的“冻结”)。
如何确保View在尊重MVVM的同时又不限制工作的同时接收进度更新?
相当简单:限制进度更新 。 或更具体地说,对它们进行采样 。
首先,我建议使用带有IProgress<T>
Filip的Task.Run
方法; 这相当于BackgroundWorker
的现代版本(有关更多信息,请参见我的博客 )。
其次,为了采样进度更新,您应该使用IProgress<T>
的实现,该实现允许您基于时间进行采样(即, 不要使用Progress<T>
)。 具有基于时间的逻辑的异步序列? Rx是明确的选择。 李·坎贝尔(Lee Campbell)有很好的执行力 ,而我有一个较小的实现 。
例如,使用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;
});
为什么要使用BackgroundWorker? 这是一个带有任务的简单进度实现,如果您访问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);
}
});
}
有关此主题的更多信息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.