Introduction
This article presents a novice .NET developer to develop a multithreading application without being burdened by the complexity that comes with threading.
Background
A basic Windows application runs on a single thread usually referred to as UI thread. This UI thread is responsible for creating/painting all the controls and upon which the code execution takes place. So when you are running a long-running task (i.e., data intensive database operation or processing some 100s of bitmap images), the UI thread locks up and the UI application turns white (remember the UI thread was responsible to paint all the controls) rendering your application to Not Responding state.
Using the Code
What you need to do is to shift 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 extremely simple:
- Create a
BackgroundWorker
object. - Tell the
BackgroundWorker
object what task 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).
BackgroundWorker
uses the thread-pool, which recycles threads to avoid recreating them for each new task. This means one should never call Abort
on a BackgroundWorker
thread.
And a golden rule never to forget:
Never access UI objects on a thread that didn't create them. It means you cannot use a code such as this...
lblStatus.Text = "Processing file...20%";
...in the DoWork
function. Had you done this, you would receive a runtime error. 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 (In our case, setting the label text status).
BackgroundWorker
also provides a RunWorkerCompleted
event which fires after the DoWork
event handler has done its job. Handling RunWorkerCompleted
is not mandatory, but one usually does so in order to query any exception that was thrown in DoWork
. Furthermore, code within a RunWorkerCompleted
event handler is able to update Windows Forms and WPF controls without explicit marshalling; code within the DoWork
event handler cannot.
To add support for progress reporting:
- Set the
WorkerReportsProgress
property to true
. - Periodically call
ReportProgress
from within the DoWork
event handler with a "percentage complete" value.
m_oWorker.ReportProgress(i);
- Handle the
ProgressChanged
event, querying its event argument's ProgressPercentage
property:
void m_oWorker_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
progressBar1.Value = e.ProgressPercentage;
lblStatus.Text = "Processing......" + progressBar1.Value.ToString() + "%";
}
Code in the ProgressChanged
event handler is free to interact with UI controls just as with RunWorkerCompleted
. This is typically where you will update a progress bar.
To add support for cancellation:
Properties
This is not an exhaustive list, but I want to emphasize the Argument
, Result
, and the RunWorkerAsync
methods. These are properties of BackgroundWorker
that you absolutely need to know to accomplish anything. I show the properties as you would reference them in your code.
DoWorkEventArgs e
Usage: Contains e.Argument
and e.Result
, so it is used to access those properties.e.Argument
Usage: Used to get the parameter reference received by RunWorkerAsync
.e.Result
Usage: Check to see what the BackgroundWorker
processing did.m_oWorker.RunWorkerAsync();
Usage: Called to start a process on the worker thread.
Here's the entire code:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Threading;
using System.Text;
using System.Windows.Forms;
namespace BackgroundWorkerSample
{
public partial class Form1 : Form
{
BackgroundWorker m_oWorker;
public Form1()
{
InitializeComponent();
m_oWorker = new BackgroundWorker();
m_oWorker.DoWork += new DoWorkEventHandler(m_oWorker_DoWork);
m_oWorker.ProgressChanged += new ProgressChangedEventHandler
(m_oWorker_ProgressChanged);
m_oWorker.RunWorkerCompleted += new RunWorkerCompletedEventHandler
(m_oWorker_RunWorkerCompleted);
m_oWorker.WorkerReportsProgress = true;
m_oWorker.WorkerSupportsCancellation = true;
}
void m_oWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
if (e.Cancelled)
{
lblStatus.Text = "Task Cancelled.";
}
else if (e.Error != null)
{
lblStatus.Text = "Error while performing background operation.";
}
else
{
lblStatus.Text = "Task Completed...";
}
btnStartAsyncOperation.Enabled = true;
btnCancel.Enabled = false;
}
void m_oWorker_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
progressBar1.Value = e.ProgressPercentage;
lblStatus.Text = "Processing......" + progressBar1.Value.ToString() + "%";
}
void m_oWorker_DoWork(object sender, DoWorkEventArgs e)
{
for (int i = 0; i < 100; i++)
{
Thread.Sleep(100);
m_oWorker.ReportProgress(i);
if (m_oWorker.CancellationPending)
{
e.Cancel = true;
m_oWorker.ReportProgress(0);
return;
}
}
m_oWorker.ReportProgress(100);
}
private void btnStartAsyncOperation_Click(object sender, EventArgs e)
{
btnStartAsyncOperation.Enabled = false;
btnCancel.Enabled = true;
m_oWorker.RunWorkerAsync();
}
private void btnCancel_Click(object sender, EventArgs e)
{
if (m_oWorker.IsBusy)
{
m_oWorker.CancelAsync();
}
}
}
}
1. Start
Once the application is started, click on the button that reads Start Asynchronous Operation. The UI now shows a progressbar with UI continuously being updated.
2. Cancel
To cancel the parallel operation midway, press the cancel button. Note that the UI thread is now free to perform any additional task during this time and it is not locked by the data intensive operation that is happening in the background.
3. On Successful Completion
The statusbar shall read Task Completed upon the successful completion of the parallel task.
Points of Interest
History
- 4th August, 2010: Initial version