The code is suffering from race conditions. The same thing would happen if multiple threads or multiple BackGroundWorker components were used, that tried to modify DisplayText from the DoWork method.
As the real platform is Winforms, I suspect DisplayText is used as a quick+dirty way to display results or log messages to a TextBox without causing a cross-thread exception.
Before explaining what's wrong, this is how the entire problem can be solved in WinForms since 2012, without blocking or explicit locking and ensure results are displayed as soon as they're available. Along with the mandatory progress and message in a status bar. All forms need a progress and status bar, it's the rules.
Async/await, Parallel.ForEachAsync and Progress
Let's assume the actual work is to retrieve the status from a bunch of sites. The progress report is the site and Status code:
record SiteUrl(string Site, string Url);
record SiteResult(string Site, HttpStatusCode Status);
In the form, progress is handled by the Progress class. Instead of generating temporary strings, the status message is appended to TextBox1. The Progress<> class ensures any progress messages are handled in the thread that created the class, in this case the UI thread. This avoids cross-thread exceptions and ensures there are no race conditions :
IProgress<SiteResult> _progress;
HttpClient _client;
public Form1()
{
InitializeComponent();
_progress = new Progress<SiteResult>(ReportProgress);
_client=new HttpClient();
}
private void ReportProgress(SiteResult result)
{
var msg = $"{result.Site}:{result.Status}";
textBox1.AppendText("\r\n");
textBox1.AppendText(msg);
toolStripStatusLabel1.Text = msg;
toolStripProgressBar1.Increment(1);
}
Instead of the blocking Parallel.ForEach, the asynchronous Parallel.ForEachAsync is used :
record SiteUrl(string Site,string Url);
private async void button1_Click(object sender, EventArgs e)
{
var sites = new List<SiteUrl>{
new SiteUrl("Google","https://www.google.com"),
new SiteUrl("MS","https://www.microsoft.com"),
new SiteUrl("FB","https://www.facebook.com"),
};
// Noisy stuff, extract it to keep the code clean
ResetProgress(sites.Count);
await Parallel.ForEachAsync(sites, async (site, ct) => ProcessSite(site, ct));
}
async Task ProcessSite(SiteUrl site,CancellationToken ct)
{
var result = await _client.GetAsync(site.Url, ct);
_progress.Report(new SiteResult(site.Site, result.StatusCode));
//Do something else with the result ...
}
private void ResetProgress(int max)
{
textBox1.Text= "Sites= ";
toolStripStatusLabel1.Text="";
toolStripProgressBar1.Value=0;
toolStripProgressBar1.Maximum=max;
}
What's wrong
The original code suffers from race conditions. The threads used by Parallel.ForEach are reading the same value from DisplayText at the same time, modifying it and then replacing DisplayText with their own value. The last one to make that replacement wins.
One way to fix this is by using a lock to protect access to the field,eg :
//Lock object stored in a field
object _lockObj=new object();
...
lock(_lockObj)
{
DisplayText = DisplayText + "," + text;
}
That's not the best option, as it blocks the worker threads while trying to update the status field.
As the real change we want is to update textbAnother option is to forward the status change to the UI thread with BeginInvoke`.
if (retryCount == maxRetryCount - 1)
{
textBox1.BeginInvoke(() =>UpdateStatus(text));
break;
}
...
void UpdateStatus(string text)
{
textBox1.AppendText("," + text);
}
DisplayText is no longer needed, the worker threads aren't blocked and the status box is updated immediatelly. The code is more complex though and needs direct access to the UpdateStatus method.
Parallel.ForEach. The code modifies the sharedDisplayTextwithout synchronization which means the actual results are essentially random. Multiple threads read theDisplayTextcontents at the same time and then replace it with their own version. The last one to do so wins. You'd have to use alockstatement to ensure threads don't interfere with each other.var responses=await Task.WhenAll(urls.Select(url=>client.GetStringAsync(url)));. Not the best way, does the job and retries automaticallybutton1_Click(object sender, EventArgs e)is this WebForms? That's the only framework that uses events like this