Introduction
The need for background threading is most noticeable in the design of User
Interfaces. This is certainly not a new phenomenon. Unresponsive interface
issues have been plaguing users and developers alike since the inception of
the GUI; at least in the Microsoft Windows arena.
There are no silver bullets which resolve the problem but framework improvements have
certainly made it much easier for developers to deal with these types of
issues. The tools available for dealing with non-deterministic operations have
been improving with each release of the .NET framework. Release 1.1 provided
some robust and diverse threading capabilities but it still didn't integrate
smoothly with UI code due to thread affinity issues.
In summary, well matured and entrenched STA (Single Threaded Apartment) design
constraints permeate throughout Window based controls. This means that only the
thread that created the control may manipulate the control. Deviation from this
constraint can and will lead to very strange behaving applications. If they
behave at all!
The IDE (Integrated Development Environments) which has made the creation of User
Interfaces far easier has also made it very easy for code to be developed such
that it all runs on the main thread. In and of itself this isn't a problem
until the UI activates a long running or non-deterministic operations such as
getting data from a database or invoking a web service. All of a sudden, the
user experiences slow responses from the application or worse, the dreaded, not
responding message in the title bar.
With the tools currently available it is this authors opinion that only control
related operations which can respond within 15 to 20 ms (guaranteed) should be
executed on the main thread. All other operations should be done on alternate
threads. This seemingly Draconian measure looks to be evolving more and more into
standard practice. This design constraint is bolstered significantly by the
framework improvements that have been coming to market such as true
asynchronous ADO operations and the introduction of the Background Worker
thread.
Background Worker Thread
The
Background worker thread introduced in .NET 2.0 is made available on the Tool
palette for Form design. This new tool component has been designed such that it
aligns seamlessly with the event handling paradigm used by all the other
controls.
The
Background worker control is a very interesting when you consider that it has
encapsulated all thread affinity issues. It makes it almost too easy to forget
you are working across threads.
BackgroundWorker Object Summary
This
object can be found within the toolbox for easy dropping on a form. Removes
threading concerns while allowing for easy UI updating. The properties of a
background worker include:
WorkerSupportsCancellation
WorkerReportsProgress
CancellationPending
Methods
The RunAsynch
method starts the processing of the
code. The work is performed on the worker thread within the DoWork
event
handler. The worker should check the CancellationPending
attribute frequently.
The ProgressChanged
and RunWorkerComplete
methods
both run on the UI thread. (See diagram)
UI
Thread Worker
Thread
The
event handling methods added to the derived Form
class look like any other
methods. The intriguing and somewhat disconcerting aspect is that there isn't
any indication that you are dealing with different threads. I am not saying
this is a bad thing. The code is clean and succinct. It is just important to keep in the back of your mind that
dead-lock, synchronization and race condition issues are factors you will need
to consider when interacting between the main thread and background thread.
Basic Code Structure
void worker_DoWork (Object sender, DoWorkEventArgs e)
{
while (!worker.CancellationPending)
{
worker.ReportProgress (percentProgress);
}
};
void worker_ProgressChanged(Object sender,
ProgressChangedEventArgs e)
{
progressTextBox.Text = e.ProgressPercentage + "%";
};
void worker_RunCompleted(Object sender,
RunWorkerCompletedEventArgs e)
{
progressTextBox.Text = "Complete";
};
During debugging you can clearly see your background thread in action.
Main Thread
Worker
Thread
Some Real World Code
I mentioned earlier that ADO has provided true asynchronous capabilities. In
prior incarnations of ADO, asynchronous operations were available but at the
cost of spawning and blocking on a synchronous thread. ADO 2.0 doesn't work
that way. It is truly asynchronous.
I
decided to blend the ADO feature together with the background thread and the
binding ability of Generics to controls. The results were outstanding. I
decided to select a long running operation that would significantly disrupt the
UI if executed on the main thread. The results yield a very responsive GUI.
Please bear in mind the operation isn't any faster but the perception of
usability greatly enhances the application. Depending upon the application, it
might actually let the user do something else while she waits for the data.
The
DoWork event handler shows the invocation of a length database stored procedure
call. The points to take away are that the code checks for a user initiated
cancellation and post progress messages frequently.
private void backgroundWorker1_DoWork(object sender,
DoWorkEventArgs e)
{
Object state;
BackgroundWorker bw = sender asBackgroundWorker;
int percentProgress = 0;
Interlocked.Exchange(ref percentProgress, tickCounter);
string cnString = "Integrated Security=SSPI;
Persist Security Info=True;
Initial Catalog=pdm_snapshots;
DataSource=pm-db-dev,10501;
Asynchronous Processing=true";
state = (string)"Running query...";
using (SqlConnection cn = newSqlConnection(cnString))
{
try
{
string proc = "spGetGroup";
SqlCommand cmd = newSqlCommand(proc, cn);
cn.Open();
IAsyncResult result = cmd.BeginExecuteReader();
while ((!result.IsCompleted) &&
(!bw.CancellationPending))
{
percentProgress = tickCounter;
bw.ReportProgress(percentProgress, state);
Thread.Sleep(5);
}
if (bw.CancellationPending)
{
cmd.Cancel();
e.Cancel = true;
return;
}
percentProgress = Interlocked.Exchange(
ref tickCounter, 0);
state = (string)"Reading...";
using (SqlDataReader rdr = cmd.EndExecuteReader(
result))
{
while (rdr.Read() &&
(!bw.CancellationPending))
{
data.Add((int)rdr.GetValue(0));
percentProgress = tickCounter;
bw.ReportProgress(percentProgress, state);
Thread.Sleep(5);
}
if (bw.CancellationPending)
e.Cancel = true;
if (!rdr.IsClosed)
rdr.Close();
}
}
catch (InvalidOperationException ex)
{
throw ex;
}
catch (SqlException ex)
{
throw new InvalidOperationException(ex.Message);
}
}
Upon conclusion of the worker thread, the Run
worker completed event is
called on the main thread. The interesting bits to take away are that thrown
events from the background worker are caught and may be tested through the
RunWorkerCompletedEventArgs
object.
The code also depicts a Generic (List<int>
) bind to a control. The
Generic serves as the shared memory
construct operated upon by both the worker thread and the main Ui thread.
private void backgroundWorker1_RunWorkerCompleted(object
sender, RunWorkerCompletedEventArgs e)
{
if (e.Error != null)
{
lblPercent.Text = "Error encountered :
" + e.Error.Message;
}
else
if (e.Cancelled)
{
lblPercent.Text = "Operation cancelled!";
}
timer1.Enabled = false;
Interlocked.Exchange(ref tickCounter, 0);
progressBar1.Value = 0;
lstData.UseWaitCursor = false;
btnGetData.Enabled = true;
BindingSource bs = newBindingSource();
bs.DataSource = data;
lstData.DataSource = bs;
}
Conclusion
As we accelerate into the realm of multi-cores and exceedingly more
complex and loosely coupled systems, the need for easy to use threading
functions is ever increasing. The .NET framework is evolving to meet those and
other challenges. The background worker thread is but one small example of a
productivity enhancement which can be leveraged to provide the best user
experiences for our customers.