简体   繁体   中英

In MVVM, how can I prevent BackgroundWorker from freezing UI?

First of all, I've seen many similar issues here on SO and around the net. None of these appear to address my particular issue.

I have a simple BackgroundWorker whose job is to read a file line-by-line and report progress to indicate how far through it is. There are up to a total of 65,553 lines in the file, so it is important to me that the BackgroundWorker finishes as fast as possible.

Since MVVM is built on separation of concerns (SoC) and the decoupling of the View and View-Model, the BackgroundWorker updates properties on the View-Model that the View binds to. My setup is very similar to Kent Boorgaart's answer on another question.

In high-stress scenarios where the BackgroundWorker demands a lot of CPU without sleeping, the UI thread is starved and not able to update any of the bound properties that have been notified via INotifyPropertyChanged . However, if the BackgroundWorker sleeps, then the job will not finish as fast as possible.

How can I ensure the View receives progress updates while respecting MVVM and while not throttling the job?

In the View-Model the BackgroundWorker is setup like this. The Start() function is called by a RelayCommand (part of 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();
}

Here is the code for the actual work performed:

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;
    }
}

In the above code, the following properties are used for bindings between the View and View-Model and each will trigger INotifyPropertyChanged.PropertyChanged events:

  • Progress
  • Task
  • IsAnalyzing


EDIT:

In follow up Stephen Cleary and Filip Cordas, I've attempted using Task.Run() with and without ObservableProgress .

I've simplified the background task to iterate through numbers instead of lines of a file.

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;
}

Now, I start the task in either one or two ways (with or without the 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;
        });
    }
}

In both scenarios (with or without ObservableProgress ) I find that I still need to throttle the background job by using Thread.Sleep(5) . Otherwise the UI freezes.


EDIT 2:

I made a small modification to progress reports inside the worker thread:

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)));
    }
}

With this modification, the UI does not lock anymore and changes are propagating properly. Why is this so?

In high-stress scenarios where the BackgroundWorker demands a lot of CPU without sleeping, the UI thread is starved and not able to update any of the bound properties that have been notified via INotifyPropertyChanged. However, if the BackgroundWorker sleeps, then the job will not finish as fast as possible.

Having a background thread use CPU won't interfere with the UI thread. What I suspect is actually happening is that the background thread is sending progress updates too quickly to the UI thread, and the UI thread is simply unable to keep up. (This ends up looking like a complete "freeze" because of the way Win32 messages are prioritized).

How can I ensure the View receives progress updates while respecting MVVM and while not throttling the job?

Rather simple: throttle the progress updates . Or more specifically, sample them.

First, I recommend using Filip's approach of Task.Run with IProgress<T> ; this is the modern equivalent of BackgroundWorker (more info on my blog ).

Second, in order to sample the progress updates, you should use an implementation of IProgress<T> that allows you to sample based on time (ie, don't use Progress<T> ). Asynchronous sequences with time-based logic? Rx is the clear choice. Lee Campbell has a great implementation , and I have a lesser one .

Example, using Lee Campbell's 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;
    });

Why are you using BackgroundWorker? Here is a simple progress implementation with tasks and it won't block UI thread if you access PropertyChanged invoke

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);
                }
            });
        }

Some more info on the subject https://blogs.msdn.microsoft.com/dotnet/2012/06/06/async-in-4-5-enabling-progress-and-cancellation-in-async-apis/ For async programing

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM