Introduction
This article is for novice and amateur developers who recognize areas of their application that could use threading but don't want to be burdened with some of the complexity that comes with threading. Threading is a concept that many programmers tend to avoid because it can be difficult to understand, debug and implement. You can develop a very sophisticated multi-threaded application using C#. Don't worry, the BackgroundWorker
object makes threading easy to use even if you don't want to take the time to understand everything about threading.
Using the Code
When your application loads, it runs on a single thread. This is referred to as the UI thread. Generally this is the thread that all of your UI objects have been created on and this is the thread that all of your code execution is performed on. The UI also uses this single thread to paint the UI objects. So when you're running a long task, like processing some unknown number of MP3 files in a directory, your application locks up, the window turns white, the user can't click any buttons, the title bar changes to "My Cool App (Not Responding)." So you go back and put in a bunch of Application.DoEvents()
calls into your MP3 crunching function and all is better again… not really, the code runs slower now and the form still locks up but only for short spurts. The whole application seems a bit choppy.
What you need to do is this heavy processing on a different thread. Leave the UI thread free for painting the UI. .NET has made the BackgroundWorker
object available to us to simplify threading. This object is designed to simply run a function on a different thread and then call an event on your UI thread when it's complete. The steps are simple, create a BackgroundWorker
object, tell it what function to run on the background thread (the DoWork
function), tell it what function to run on the UI thread when the work is complete (the RunWorkerCompleted
function), then tell the BackgroundWorker
object to go to work.
There is one rule you need to be aware of - you can't access UI objects on a thread that didn't create them. Therefore you would receive a runtime error if you wrote the line of code lblStatus.Text = "Processing file 5 of 100";
in the DoWork
function. There are two ways around this and I use both in the examples. The BackgroundWorker
object resolves this problem by giving us a ReportProgress
function which can be called from the background thread's DoWork
function, this will cause the ProgressChanged
event to fire on the UI thread. Now we can access the UI objects on their thread and do what we want. But what if I just need to update a label or disable a button while on the background thread, no problem, using Control.Invoke
you can supply some code (in an anonymous function) to be run on the UI thread, I use this technique in the asynchronous example to update the Progress Form's label and progress bar.
Synchronous Example
There are two examples in this article, one on synchronous threading and the other on asynchronous threading. The idea behind the synchronous example is that you want to disable your application so the user can't do anything else, but you want the application to still paint while you notify the user of the progress and give him or her the ability to cancel the process.
public partial class fmMain : Form
{
private fmProgress m_fmProgress = null;
#region Synchronous BackgroundWorker Thread
private void bnSync_Click( object sender, EventArgs e )
{
BackgroundWorker bw = new BackgroundWorker();
bw.DoWork += new DoWorkEventHandler( bw_DoWork );
bw.RunWorkerCompleted += new RunWorkerCompletedEventHandler
( bw_RunWorkerCompleted );
m_fmProgress = new fmProgress();
bw.RunWorkerAsync();
m_fmProgress.ShowDialog( this );
m_fmProgress = null;
}
private void bw_DoWork( object sender, DoWorkEventArgs e )
{
int iCount = new Random().Next( 20, 50 );
for( int i = 0; i < iCount; i++ )
{
Thread.Sleep( 100 );
m_fmProgress.lblDescription.Invoke((MethodInvoker) delegate()
{
m_fmProgress.lblDescription.Text =
"Processing file " + i.ToString() +
" of " + iCount.ToString();
m_fmProgress.progressBar1.Value =
Convert.ToInt32( i * ( 100.0 / iCount ) );
});
if( m_fmProgress.Cancel )
{
e.Cancel = true;
return;
}
}
}
private void bw_RunWorkerCompleted
( object sender, RunWorkerCompletedEventArgs e )
{
if( m_fmProgress != null )
{
m_fmProgress.Hide();
m_fmProgress = null;
}
if( e.Error != null )
{
MessageBox.Show( e.Error.Message );
return;
}
if( e.Cancelled )
{
MessageBox.Show( "Processing cancelled." );
return;
}
MessageBox.Show( "Processing is complete." );
}
#endregion
}
Asynchronous Example
The purpose of the asynchronous example is to allow the user to kick off a process while still having the ability to continue using your application. However, the user still needs to have the ability to determine the progress of the process and the ability to cancel it. In this example we don't want to display modal for that which disables the application.
public partial class fmMain : Form
{
private BackgroundWorker m_AsyncWorker = new BackgroundWorker();
public fmMain()
{
InitializeComponent();
m_AsyncWorker.WorkerReportsProgress = true;
m_AsyncWorker.WorkerSupportsCancellation = true;
m_AsyncWorker.ProgressChanged += new ProgressChangedEventHandler
( bwAsync_ProgressChanged );
m_AsyncWorker.RunWorkerCompleted += new RunWorkerCompletedEventHandler
( bwAsync_RunWorkerCompleted );
m_AsyncWorker.DoWork += new DoWorkEventHandler( bwAsync_DoWork );
}
#region Asynchronous BackgroundWorker Thread
private void bnAsync_Click( object sender, EventArgs e )
{
if( m_AsyncWorker.IsBusy )
{
bnAsync.Enabled = false;
lblStatus.Text = "Cancelling...";
m_AsyncWorker.CancelAsync();
}
else
{
bnAsync.Text = "Cancel";
lblStatus.Text = "Running...";
m_AsyncWorker.RunWorkerAsync();
}
}
private void bwAsync_DoWork( object sender, DoWorkEventArgs e )
{
BackgroundWorker bwAsync = sender as BackgroundWorker;
int iCount = new Random().Next( 20, 50 );
for( int i = 0; i < iCount; i++ )
{
Thread.Sleep( 100 );
bwAsync.ReportProgress( Convert.ToInt32( i * ( 100.0 / iCount )));
if( bwAsync.CancellationPending )
{
Thread.Sleep( 1200 );
e.Cancel = true;
return;
}
}
bwAsync.ReportProgress( 100 );
}
private void bwAsync_RunWorkerCompleted
( object sender, RunWorkerCompletedEventArgs e )
{
bnAsync.Text = "Start Long Running Asynchronous Process";
bnAsync.Enabled = true;
if( e.Error != null )
{
MessageBox.Show( e.Error.Message );
return;
}
if( e.Cancelled )
{
lblStatus.Text = "Cancelled...";
}
else
{
lblStatus.Text = "Completed...";
}
}
private void bwAsync_ProgressChanged
( object sender, ProgressChangedEventArgs e )
{
progressBar1.Value = e.ProgressPercentage;
}
#endregion
}
Points of Interest
Check out my other posts:
About the Author
Andrew D. Weiss
Software Engineer
Check out my blog: More-On C#