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):
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:
- Why does the Task.Delay version of the program not deadlock?
- Why are multiple threads produced to handle the BackgroundWorker.ProgressChanged event?
- How can I easily guarantee that user state is received in the order that it is sent?