Oct 2011

Avoiding Race Conditions with a BackgroundWorker

by Steve Wortham
Today I released an update to Regex Hero, adding asynchronous regex matching and highlighting. This is something I've attempted in the past. But my past attempts resulted in a race condition.

What is a race condition you say? Well, simply put, it's when two or more threads race to an event or line of code. Sometimes one thread will win the race, and sometimes the other thread will. What this comes down to is that this inconsistent behavior can cause some strange and unexpected results.

Now, often times when I've used a BackgroundWorker, it's for a fairly long running task. The task executes and the user can cancel if they want (which will call CancelAsync behind the scenes). And everything's just fine.

But what if the application calls CancelAsync() very frequently (after every keypress), and every time it does you want the BackgroundWorker to stop, and then start over? And what if the execution time ranges from a couple milliseconds to a couple seconds? Well, as I found, these characteristics can be problematic.

From MSDN's documentation for the BackgroundWorker.CancelAsync Method:
Be aware that your code in the DoWork event handler may finish its work as a cancellation request is being made, and your polling loop may miss CancellationPending being set to true. In this case, the Cancelled flag of System.ComponentModel.RunWorkerCompletedEventArgs in your RunWorkerCompleted event handler will not be set to true, even though a cancellation request was made. This situation is called a race condition and is a common concern in multithreaded programming. For more information about multithreading design issues, see Managed Threading Best Practices.

This is exactly what happened to me. With every keypress I'd check the BackgroundWorker IsBusy flag. And if it was busy I'd call CancelAsync(). Then if the Cancelled flag was set to true, I'd call RunWorkerAsync() to start it over. The thing is, as noted above, calling CancelAsync() doesn't guarantee that the Cancelled flag will be set to true. And this messed everything up since it meant that the last call to the BackgroundWorker wasn't always made. So if you're familiar with Regex Hero, you can imagine a situation where the highlighted matches aren't accurate because it's not in sync. And that's bad.

But of course I found something that works, and that's what this post is all about. I've rewritten the code and made a simple example to make it easier to follow. This example is in Silverlight, but of course the same could be done in the full .NET Framework.




I start by declaring a few variables and initializing the BackgroundWorker...


private Boolean PerformHashRetry = false;
private HMACSHA256 sha256 = new HMACSHA256();
private UTF8Encoding utf8 = new UTF8Encoding();
private BackgroundWorker PerformHashWorker = new BackgroundWorker();

public MainPage()
{
InitializeComponent();

PerformHashWorker = new BackgroundWorker();
PerformHashWorker.WorkerSupportsCancellation = true;
PerformHashWorker.DoWork += PerformHash_DoWork;
PerformHashWorker.RunWorkerCompleted += PerformHash_Completed;

txtBox.TextChanged += new TextChangedEventHandler(txtBox_TextChanged);
}

void txtBox_TextChanged(object sender, TextChangedEventArgs e)
{
PerformHash();
}

Here's my PerformHash() function, which decides whether to run the background worker, or cancel it and signal it to run again...


void PerformHash()
{
if (PerformHashWorker.IsBusy)
{
PerformHashRetry = true;
PerformHashWorker.CancelAsync();
}
else
{
PerformHashRetry = false;
PerformHashWorker.RunWorkerAsync(txtBox.Text);
}
}

Then the PerformHash_DoWork() function is pretty straightforward. I've set it up for 10,000 iterations so the delay will be noticeable. However, notice how it checks for the CancellationPending flag, so it can terminate early if necessary...


void PerformHash_DoWork(object sender, DoWorkEventArgs e)
{
for (int i = 0; i < 10000; i++)
{
Byte[] HashValue = sha256.ComputeHash(utf8.GetBytes((string)e.Argument + i.ToString()));
e.Result = utf8.GetString(HashValue, 0, HashValue.Length);

if (PerformHashWorker.CancellationPending)
{
e.Cancel = true;
return;
}
}
}

Last, but certainly not least, there's the PerformHash_Completed() function...


void PerformHash_Completed(object sender, RunWorkerCompletedEventArgs e)
{
if (PerformHashRetry || e.Cancelled) // instead of relying solely on the e.Cancelled flag, we're also using our global PerformHashRetry variable
{
// This is where the magic happens.
// If the BackgroundWorker has been flagged for a retry,
// then we call PerformHash to start this whole process over.
// By doing it this way, race conditions can be avoided and
// the last call to the BackgroundWorker will always be made.
PerformHash();
return;
}

txtHash.Text = (string)e.Result;
}

Download AvoidingRaceConditions.zip (Visual Studio 2010 project) to try this out yourself.



comments powered by Disqus