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