【问题标题】:In MVVM, how can I prevent BackgroundWorker from freezing UI? [duplicate]在 MVVM 中,如何防止 BackgroundWorker 冻结 UI? [复制]
【发布时间】: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() 函数由 RelayCommandMVVM-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 事件:

  • Progress
  • Task
  • IsAnalyzing


编辑:

在跟进 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 你的建议似乎解决了这个问题。你能解释一下为什么过于频繁地报告进度(BackgroundWorkerIProgress)会导致 UI 锁定吗?
  • 想一想……如果你在UI线程中运行一个持久的任务,那么UI线程是无法更新可视化窗口的。它将显示为锁定状态,并且不会响应鼠标点击等事件。类似地,如果你每隔几毫秒就在 UI 线程上调用一个方法,那么 UI 线程除了运行方法之外什么也做不了。它无法更新 UI,因为糟糕,这是我必须运行的另一个更新。此外,如此快地运行更新是没有意义的,人眼不会看到中间步骤。

标签: c# wpf mvvm data-binding backgroundworker


【解决方案1】:

在 BackgroundWorker 需要大量 CPU 而不休眠的高压力场景中,UI 线程处于饥饿状态,无法更新已通过 INotifyPropertyChanged 通知的任何绑定属性。但是,如果 BackgroundWorker 处于休眠状态,则作业不会尽快完成。

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

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

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

首先,我推荐使用 Filip 的方法Task.RunIProgress&lt;T&gt;;这是BackgroundWorker 的现代版(更多信息on my blog)。

其次,为了对进度更新进行采样,您应该使用IProgress&lt;T&gt; 的实现,它允许您根据时间进行采样(即不要使用Progress&lt;T&gt;)。具有基于时间的逻辑的异步序列? Rx 是明确的选择。 Lee Campbell 有一个great implementation,我有一个lesser one

例如,使用 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;
    });

【讨论】:

  • 感谢您分享您的博文,我完全相信我应该使用 Task.Run 进行异步编程。我已经按照您的建议实施了 Rx 并执行了进度抽样。但是,我发现仍然需要限制任务才能将更改传播到 UI。当我限制任务时,我仍然能够看到采样率生效。
  • @NickMiller:这不应该发生,除非您的后台工作以其他方式中断了 UI 线程。
  • 我已经编辑了 OP 并找到了似乎导致 UI 锁定的原因(请参阅编辑 2),但我仍然没有任何解释。 UI 和后台线程交互的唯一点是后台线程何时生成以及后台线程在进度报告期间修改 UI 绑定属性时(依次通过INotifyPropertyChanged 通知)。
【解决方案2】:

为什么要使用 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/异步编程的信息

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多