0

I am experiencing a problem with a background worker in which the ProgressChangedEvents of the same background worker are handled on different worker threads.

Locally, I am using .Net Framework 4.7.2.

I include a minimal example that reproduces the issue below. To ensure that the problem occurs, I make the first ProgressChanged event take far longer to handle than the second by enumerating a 10-million-long sequence in it.

using System;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace EventOrderingIssue
{
    public class Program
    {
        static BackgroundWorker backgroundWorker;
        static int reportCount = 0;
        static List<string> received = new List<string>();

        public static void Main(string[] args)
        {
            backgroundWorker = new BackgroundWorker()
            {
                WorkerReportsProgress = true,
            };

            backgroundWorker.DoWork += BackgroundWorker_DoWork;
            backgroundWorker.ProgressChanged += BackgroundWorker_ProgressChanged;
            backgroundWorker.RunWorkerAsync();
            while (reportCount != 2)
            {
                Task.Delay(100).Wait();
                // Alternatively, Application.DoEvents();
            }
            Console.WriteLine(string.Join("\n", received));
        }

        private static void BackgroundWorker_DoWork(object sender, DoWorkEventArgs e)
        {
            IEnumerable<string> hugeState = Enumerable.Range(0, 10000000).Select(i => $"String {i}");
            backgroundWorker.ReportProgress(1, hugeState);
            backgroundWorker.ReportProgress(99, new string[] { "Last" });;
        }

        private static void BackgroundWorker_ProgressChanged(object sender, ProgressChangedEventArgs e)
        {
            received.Add(e.ProgressPercentage.ToString());
            received.Add(((IEnumerable<string>)e.UserState).Last());
            reportCount++;
        }
    }
}

Or, as a dotnetfiddle: https://dotnetfiddle.net/FFI3DO (though the Application.DoEvents() version wouldn't be runnable here).

My understanding is that with Task.Delay(100).Wait(); the program should deadlock and with Application.DoEvents(); the program should produce the output:

1
String 9999999
99
Last

instead (with either), the program produces the output

1
99
Last
String 9999999

I was confused by what was happening here, so I looked at it in the Visual Studio debugger.

It appears that the background worker is running on different threads (see the two separate threads in BackgroundWorker_ProgressChanged):

Two different threads handling progress changed

To me, this seems like the background worker is misbehaving. According to https://learn.microsoft.com/en-us/dotnet/api/system.componentmodel.backgroundworker.reportprogress?view=netframework-4.7.1, for the ReportProgress(Int32) overload: "The ProgressChanged event handler executes on the thread that created the BackgroundWorker". I should have thought that this should apply to the ReportProgress(Int32, Object) overload as well.

If BackgroundWorker_ProgressChanged can be handled in different threads, then useful guarantees about ordering are lost. Of course, it would be possible to number all of the user states as they are sent and accept them into the list in the order of their numbering, but this would be a lot of awkward code for something that I should have thought a background worker would provide for free.

My questions are as follows:

  1. Why does the Task.Delay version of the program not deadlock?
  2. Why are multiple threads produced to handle the BackgroundWorker.ProgressChanged event?
  3. How can I easily guarantee that user state is received in the order that it is sent?
Sepia
  • 447
  • 6
  • 21
  • 1
    I think the answer is in the linked question. The documentation seems to refer to the usual case of using a BackgroundWorker with a UI thread, so is confusing when used outside that. – Tim Rogers Nov 20 '20 at 12:57
  • @TimRogers That is indeed the issue - thank you very much for finding it. A simple (though inelegant) solution then is to start the main method with "using WindowsFormsSynchronizationContext synchronizationContext = new WindowsFormsSynchronizationContext(); SynchronizationContext.SetSynchronizationContext(synchronizationContext);" – Sepia Nov 20 '20 at 13:40
  • 1
    Two points not related with the main issue: 1. This background worker does essentially no work in the background (in the `DoWork` handler). It just creates a deferred `IEnumerable`. The enumeration of this enumerable happens inside the `ProgressChanged` handler. 2. The `BackgroundWorker` class is technologically obsolete, and shouldn't be used for new development. Unless you are targeting a .NET Framework of the pre-async/await era (4.0 or earlier). – Theodor Zoulias Nov 20 '20 at 14:48
  • @TheodorZoulias Thanks for your thoughts. RE: 1 - yes, that is entirely intentional. The point of the example is to ensure that handling the first ProgressChanged takes longer than the second. The real code that motivates this question has a great deal going on in the background worker. – Sepia Nov 20 '20 at 17:24
  • RE: 2 - I understand async/await is the preferred way of dealing with asynchronicity. In the real tool, a background worker is a better fit conceptually than async/await: a class (UI or CLI runner) needs to start a long-running, complicated task that must intermittently report progress (via a percentage and state). Upgrading to async/await may be worth doing in the future, though I don’t consider it a priority currently. – Sepia Nov 20 '20 at 17:24
  • 1
    You can see [here](https://stackoverflow.com/questions/12414601/async-await-vs-backgroundworker/64620920#64620920) an example of using a combination of `Task.Run` + `Progress` + async/await to do exactly the same thing that you can do with a `BackgroundWorker`, with less code and more flexibility and safety. You could even consider making your own `IProgress` implementation, if the behavior of the built-in implementation (`Progress`) is not exactly what you need. – Theodor Zoulias Nov 20 '20 at 17:37

0 Answers0