C# Corner

Simplify Background Threads

It requires a lot of plumbing to create, manage, and communicate with background threads. The System.ComponentModel.BackgroundWorker class already contains the functionality you need to follow best practices.

Technology Toolbox: C#

Some tasks take time; nonetheless, some users frankly aren't interested in waiting. When users ask for something, they want immediate feedback. As soon as you start thinking about programming with multiple threads, you must give considerable thought to the complexity you're adding to your application.

The simplest way to navigate that complexity is to let the framework handle it for you. Using the System.ComponentModel.BackgroundWorker class, you can minimize the amount of work you need to do to support common features associated with multithreaded applications. I'll show how to do this, explaining first how to modify a single-threaded application to use the BackgroundWorker, and then how to leverage the functionality in the BackgroundWorker class to create a much more robust user experience.

The sample app performs some basic grunt work of moving files around. Like a lot of developers, I move my work around between several different computers. I've got different setups for Visual Studio 2005, Visual Studio 2008, a desktop setup, and a laptop. This means I often need to move files between these different machines. I've got lots of projects on each machine, so it can be a challenge to figure out what to copy when I'm moving from one machine to another. What did I edit? What did I create? What's new? I’m a lazy type of guy at heart, so the sample app takes care of this drudge work for me (Figure 1).

Figure 1
[Click on image for larger view.]
Figure 1. Find Modified Files. The sample app described in this article searches through directories and finds any files that have been modified since the time selected. The app itself isn’t complex, but that’s the point: It illustrates how to implement backgroundworker threads so that they can be canceled mid-process, while also protecting the underlying thread from changes while the thread itself is running.

Running the single-threaded version of the application is not a pleasant experience. The method that loops through the disk to find changed files runs in the UI thread, and doesn't ever interrupt itself. The form doesn't update itself, Windows will declare it isn't responding, and the app's users get upset, close that app, and write bug reports.

To fix this kind of behavior, you need to move the long-running task into a background thread. That way, the main thread continues to process messages and Windows doesn't think the application might have died. There are many ways to create these threads. You could use the System.Threading.Thread class and build all the infrastructure on that. Or, you could use the System.Threading.ThreadPool.QueueUserWorkItem API. That is less work, but you would have to build in the entire communication infrastructure to add many of the features that users would expect.

An easier approach still is to use the System.ComponentModel.BackgroundWorker to move this work into a background thread.

The MSDN documentation provides all the basics for using the BackgroundWorker class. Those samples emphasize the ease of using it. My opinion is that following those practices will work for simple applications, but will make it harder to avoid synchronization issues as the application grows in size. The MSDN samples all drag a BackgroundWorker onto the current form, hook up the events to private methods in the form class, and start working. That practice means that you have methods in your form class running on two different threads. Those methods can access, or modify, any of the private members in your form, leading to synchronization issues (see the article, "Write Code for a Multithreaded World," C# Corner, VSM August 2007). As you go down this path, you'll find you need to protect every member data access or modification inside that class.

I prefer to isolate the work done in a background thread into its own class, or classes. So, I create a class that handles all the background thread processing. This class communicates with the foreground thread using events when it completes. I also make sure that all the data types passed between the foreground and background threads are immutable, which guarantees that they cannot be modified in one thread while the other thread is accessing it.

Your first step to adopting such an approach is to create a class that performs the long-running task, even if it's in the same thread (see Listing 1). You set the input parameters to the long-running task in the constructor. The DoWork() method does the work, even if it's on the current thread. After calling DoWork(), callers can retrieve the results.

Once you've got this basic structure in place, you can add the BackgroundWorker and perform the task asynchronously.

Obviously, you need to add a BackgroundWorker to the FindFilesWorker class. Make it a static member, because you need only one, regardless of how many background tasks you will eventually perform. That's true for this sample because I'll modify the UI so that only one file find task is performed at once. If you wanted to start multiple background tasks, you would make the BackgroundWorker an instance member, and create a new BackgroundWorker for each FindFilesWorker.

You must handle the DoWork event raised by the BackgroundWorker once you're performing tasks in the background. You should also handle the OnCompleted event raised by the BackgroundWorker. That's the only way you know that an operation has completed. Yo­ur constructor now handles that extra logic:

public FindFilesWorker(string root, TimeSpan start)
{
	rootDir = root;
	epoch = start;

	if (engine == null)
	{
		engine = new BackgroundWorker();
		engine.DoWork += new 
			DoWorkEventHandler(
				engine_DoWork);
		engine.RunWorkerCompleted += new 
			RunWorkerCompletedEventHandler(
			engine_RunWorkerCompleted);
	}
}

Next, you need to modify the FindFilesWorker class to support asynch operations. You can add a StartWork() method to start the task using the BackgroundWorker:

public bool StartWork()
{
	if (!engine.IsBusy)
	{
		engine.RunWorkerAsync(this);
		return true;
	}
	else
		return false;
}

This method first checks to see if the BackgroundWorker is already busy. If it is already running another task, StartWork fails. Otherwise, StartWork calls RunWorkerAsync to start the long-running task. RunWorkerAsync raises the DoWork event, using the parameter you give RunWorkerAsync as the Argument member of the DoWorkEventArgs structure. You use that in your OnDoWork event handler to perform the task in the background:

private static void engine_DoWork(object sender, 
	DoWorkEventArgs e)
{
	FindFilesWorker args = e.Argument as 
		FindFilesWorker;
	args.DoWork();
}

You're already doing the work in the background. All that's left to do is to fill in the OnWorkCompleted event handler. This approach does introduce a bit of coding overhead, because the purpose is to communicate results to the object that created this worker object. This means you're going to raise your own event when the task is done. Also, this is a bit of an interim step; you'll remove some of this inefficiency later in the process.

The OnWorkCompleted event handler checks whether anyone has registered against the FindFilesWorker's OnEndWork event, and notifies the developer of any changes:

void engine_RunWorkerCompleted(object sender, 
	RunWorkerCompletedEventArgs e)
{
	if (OnEndWork != null)
	{
		OnEndWork(this, new 
			FindFilesCompletedEventArgs(
			fileStats));
	}
}

The OnEndWork event definition and the FindFilesCompletedEventArgs to accomplish this are straightforward (see Listing 2).

Using the FindFilesWorker as a background task requires a few changes to the caller. The Go button handler in the main form now must register for the results using the worker's event mechanism:

private void buttonGo_Click(object sender, EventArgs e)
{
	fileInfoBindingSource.Clear();
	TimeSpan since = DateTime.Now – 
		dateTimePickerEpoch.Value;
	FindFilesWorker worker = new 
		FindFilesWorker(rootDir, since);
	worker.OnEndWork += new 
		EventHandler<FindFilesCompletedEventArgs>
		(worker_OnEndWork);
	this.Cursor = Cursors.WaitCursor;
	buttonGo.Enabled = false;
	worker.StartWork();
}

And, of course, you need to implement that event hander:

void worker_OnEndWork(object sender, 
	FindFilesCompletedEventArgs e)
{
	FindFilesWorker source = sender as FindFilesWorker;
	foreach (FileInfo file in e.Results)
		fileInfoBindingSource.Add(file);
	this.Cursor = Cursors.Default;
	this.buttonGo.Enabled = true;
	source.OnEndWork -= new 
		EventHandler<FindFilesCompletedEventArgs>
		(worker_OnEndWork);
}

The Go button handler should be straightforward for you to implement. It queues up a worker, registers the completion event, and starts it up. The EndWork event handler examines the results and unhooks the event. Note that you must unhook the event handler, so the Worker object can be garbage collected. Leaving the event handler hooked up would mean that the worker was still reachable and couldn't be collected.

Async, Good; Progress, Better
You've made progress. The long-running task is now happening in the background, and your UI is still responsive. But your users still need to wait until the task has completed before seeing any results. This runs counter to the idea of making this long-running task run in the background.

The BackgroundWorker class already has built the infrastructure for reporting progress. All you need to do is hook into it. There are a few steps necessary to report progress for a background task. First, you need to tell the BackgroundWorker that your task reports progress, and hook up an event for it. You can add that code to the FindFilesWorker constructor:

if (engine == null)
{
	engine = new BackgroundWorker();
	engine.DoWork += new 
		DoWorkEventHandler(engine_DoWork);
	engine.RunWorkerCompleted += new 
		RunWorkerCompletedEventHandler( 
		engine_RunWorkerCompleted);
	engine.WorkerReportsProgress = true;
	engine.ProgressChanged += new     
		ProgressChangedEventHandler(
		engine_ProgressChanged);
}

Next, you need to modify the DoWork method so that it reports interim progress, if that's what the user requested. That process takes a bit more work, because you need to determine if the process is running synchronously or asynchronously. You can determine this by setting some state in the DoWork event handler. In fact, the simplest approach is to store the current BackgroundWorker component as a member variable when the process starts asynchronously:

private static void engine_DoWork(object sender, 
	DoWorkEventArgs e)
{
	FindFilesWorker args = e.Argument as 
		FindFilesWorker;
	args.currentBGWorker = sender as 
		BackgroundWorker;
	args.DoWork();
}

Next, DoWork can update progress, if the currentBGWorker member variable is set, or store the final results, if the variable isn't set:

public void DoWork()
{
	IEnumerable<FileInfo> themFiles = 
		FileUtilities.FilesChanged(rootDir, epoch);
	if (currentBGWorker != null)
		foreach(FileInfo file in themFiles)
			currentBGWorker.ReportProgress(0, 
			file);
	else
		fileStats.AddRange(themFiles);
}

Note that DoWork is leveraging the BackgroundWorker progress reporting mechanism to report progress. It might appear that this process runs to completion, and then reports all the progress; however, the magic of C# enumerators is at work. FilesChanged() is an enumerator method. It finds the first file that matches your search, then uses yield return to return the file. After you've done what you need with that file, the routine will continue to process the next file. That means you'll get a progress event each time the method finds a file that matches your criteria. Of course, ReportProgress raises an event that you need to handle and propagate to your client code. The OnReportProgress event handler utilizes a similar approach. It determines whether any clients have requested the progress event, and, if so, it propagates that event:

void engine_ProgressChanged(object sender, 
	ProgressChangedEventArgs e)
{
	FileInfo info = e.UserState as FileInfo;
	if (OnWorkProgress != null)
		OnWorkProgress(this, new 
		FindFilesProgressEventArgs(info));
	fileStats.Add(info);
}

The UI can now listen for that event and add each file to the grid as found. The OnProgressHandler is one line of code; all it does is add the next file to the binding source for the data grid.

Progress, Good; Cancel, Priceless
If your users accidentally start a long-running process, chances are they will one day make a mistake and start the wrong long-running process. When that happens, they'll want to cancel the process. Fortunately, the BackgroundWorker provides a little built-in functionality for handling this scenario, as well. You call BackgroundWorker.CancelAsync() to request a c­ancellation. Then, inside your worker method, you check the BackgroundWorker.CancellationPending flag periodically. If it's set, you stop processing and return.

Once your worker method returns, the BackgroundWorker raises the OnCompleted event, and everything is done. The best part of this approach is that your UI class requires little modification to support Cancel. All it needs is a hook to call BackgroundWorker.CancelAsync. The BackgroundWorker raises the event to indicate the process completed, so all the tear down code still happens in the OnWorkerCompleted event handler (see Listing 3).

Note that there is a bit of overhead associated with the separation of the BackgroundWorker and the UI, but this approach serves a useful purpose. The UI cannot change the parameters of the search once it's started. That's sealed inside another object. All progress, completion events, and cancellation events propagate using other immutable types. That prevents deadlocks or integrity errors that could be caused by reading and writing the same structure in two different threads. That overhead is necessary only when you're working with multiple threads.

About the Author

Bill Wagner, author of Effective C#, has been a commercial software developer for the past 20 years. He is a Microsoft Regional Director and a Visual C# MVP. His interests include the C# language, the .NET Framework and software design. Reach Bill at [email protected].

comments powered by Disqus

Featured

Subscribe on YouTube