【发布时间】:2016-11-14 22:09:00
【问题描述】:
首先,我在 SO 和网络上看到了许多类似的问题。这些似乎都不能解决我的特定问题。
我有一个简单的BackgroundWorker,它的工作是逐行读取文件并报告进度以指示它的进度。文件中总共有 65,553 行,所以对我来说,BackgroundWorker 尽快完成很重要。
由于 MVVM 建立在 separation of concerns (SoC) 之上,并且 View 和 View-Model 解耦,BackgroundWorker 更新了 View 绑定到的 View-Model 上的属性。我的设置与另一个问题上的Kent Boorgaart's answer 非常相似。
在BackgroundWorker 需要大量CPU 而不休眠的高压力场景中,UI 线程处于饥饿状态,无法更新通过INotifyPropertyChanged 通知的任何绑定属性。但是,如果BackgroundWorker 处于休眠状态,则作业不会尽快完成。
如何确保视图在尊重 MVVM 且不限制作业的同时接收进度更新?
在视图模型中,BackgroundWorker 是这样设置的。 Start() 函数由 RelayCommand(MVVM-Light 的一部分)调用。
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 事件:
ProgressTaskIsAnalyzing
编辑:
在跟进 Stephen Cleary 和 Filip Cordas 时,我尝试使用 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 会冻结。
编辑 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 不再锁定并且更改可以正确传播。为什么会这样?
【问题讨论】:
-
ReportProgress正在杀死你。仅每隔一段时间报告一次进度。您可以基于时间,或者每处理 1000 行报告进度。 -
@Will 你的建议似乎解决了这个问题。你能解释一下为什么过于频繁地报告进度(
BackgroundWorker或IProgress)会导致 UI 锁定吗? -
想一想……如果你在UI线程中运行一个持久的任务,那么UI线程是无法更新可视化窗口的。它将显示为锁定状态,并且不会响应鼠标点击等事件。类似地,如果你每隔几毫秒就在 UI 线程上调用一个方法,那么 UI 线程除了运行方法之外什么也做不了。它无法更新 UI,因为糟糕,这是我必须运行的另一个更新。此外,如此快地运行更新是没有意义的,人眼不会看到中间步骤。
标签: c# wpf mvvm data-binding backgroundworker