0

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?

Community
  • 1
  • 1
Nicholas Miller
  • 4,205
  • 2
  • 39
  • 62
  • 1
    `ReportProgress` is what is killing you. Only report progress every once in awhile. You can base it on time, or perhaps report progress every 1000 lines processed. –  Nov 15 '16 at 15:22
  • @Will Your suggestion seemed to fix the problem. Can you explain why reporting progress too frequently (with `BackgroundWorker` or `IProgress`) causes the UI to lock? – Nicholas Miller Nov 16 '16 at 18:16
  • Think about it... if you're running a long-lasting task in the UI thread, then the UI thread can't update the visual window. It will appear locked, and not respond to events such as mouse clicks. Similarly, if you are invoking a method on the UI thread every few milliseconds, the UI thread can't do anything other than run method. It can't update the UI because oops here's another update I have to run. Besides, there's no point running updates so quickly, the human eye won't see the intermediate steps. –  Nov 16 '16 at 19:01

2 Answers2

5

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 (i.e., 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;
    });
Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • Thanks for sharing your blog post, I am thoroughly convinced that I should use `Task.Run` with asynchronous programming. I have implemented Rx and performed sampling of the progress as you've suggested. However, I find that throttling the task is still required for changes to propagate to the UI. When I throttle the task, I am still able to see the sampling rate take effect. – Nicholas Miller Nov 16 '16 at 17:43
  • @NickMiller: That should not happen, unless your background work was interrupting the UI thread some other way. – Stephen Cleary Nov 16 '16 at 17:49
  • I've edited the OP and have found what appears to be causing the UI locking (see edit 2), yet I still have no explanation for it. The only points where the UI and background thread interact are when the background thread is spawned and when the background thread modifies the UI-bound properties during progress reports (which in turn notify via `INotifyPropertyChanged`). – Nicholas Miller Nov 16 '16 at 18:25
3

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

Filip Cordas
  • 2,531
  • 1
  • 12
  • 23