簡體   English   中英

在MVVM中,如何防止BackgroundWorker凍結UI?

[英]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處於睡眠狀態,則作業將無法盡快完成。

如何確保View在尊重MVVM的同時又不限制工作的同時接收進度更新?

在視圖模型中, BackgroundWorker的設置如下。 Start()函數由RelayCommandMVVM-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將凍結。


編輯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且不休眠的高壓力方案中,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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM