| Winforms programs often need to make call to
long running operations. These can be calculations, data access calls, or
calling web services. It's important to allow the user to interact with the
application. You may also want to display the progress of the operation to the
user.
|
| Long Running Operation |
| For this demonstration I'll be using the
following method to simulate a long running operation. |
protected void LongRunningMethod()
{
int count = 10000;
int step = 100;
while (count > 0)
{
System.Threading.Thread.Sleep(step);
count -= step;
}
}
|
| As you can see, this operation will pause for
about 10 sec while the thread sleep operations are carried out. |
| So what happens if we call this method
synchronously? The application will appear to the user to have locked up. If
the window is covered and then uncovered it will also not repaint. To
demonstrate this to yourself you can include a call to this method from any
event handler on your form and check out the results. |
| Asynchronous Operations |
| The .Net framework provides a design pattern
for asynchronous calls. This allows them to be called in a uniform manner
across different parts of the framework. Many .Net class have built-in support
for asynchronous programming such as I/O, sockets, messaging and web services.
The .Net framework makes the details of making the asynchronous calls nearly
transparent to the developer. |
| The .Net framework also provides each
application with a pool of 20 worker threads. This pool of background threads
is created when the application starts. The asynchronous pattern uses these
threads to complete the asynchronous calls. This saves the developer from
having to write the code to manage the threads. |
To make an asynchronous call a call to the
BeginOperation is called. The caller also needs to decide on the completion
notification technique to use. The notification techniques available are:
-
Using a callback delegate. The delegate will be invoked when the operation is
completed.
-
Poll to check for completion. The IAsyncResult interface's IsCompleted property
can be checked.
-
Call the EndOperation. This will cause the current thread to be blocked until
the operation is complete.
-
Wait on a handle. Wait on the IAsynchResult interface's WaitHandle property.
Then the EndOperation can be called.
|
| It's important to note that when using the
BeginOperation that the EndOperation must be called to prevent potential memory
leak issues. |
| Asynchronous Callback |
When using the asynchronous callback there are
four main steps in the process.
-
Create the asynchronous delegate to point back to the methods we want to call
when the operation completes. The AsyncCallbackDelegate has the following
signature void AsyncCallback( IAsyncresult ar)
-
Initiate the asynchronous call by using the BeginOperation passing it the async
delegate.
-
Inside the callback call the appropriate EndOperation method to retrieve the
results.
-
Return control to the main thread to update the user interface.
|
| Item 4 is very important. The UI can only be
safely updated from the UI thread. The UI should not be updated by any other
thread. |
| The async callback can be created from any
method that matches the delegate. The following method can be used to create
our callback. |
public void CallBackDel(IAsyncResult ar)
{
}
|
| To create the async callback we use the
following. |
| AsyncCallback ac = new
AsyncCallback(CallBackDel);
|
| The async callback is then passed to the begin
method of our delegate. |
MethodInvoker mi = new
MethodInvoker(LongRunningMethod);
mi.BeginInvoke(ac, null);
|
| In order to retrieve the results on the
completion notification we change the callback method to the following. Since
we do not have any return data we do not have to perform any assignment. If the
async call generates any exceptions, the call to EndInvoke will cause them to
be raised. |
protected void CallBackDel(IAsyncResult ar)
{
mi.EndInvoke(ar);
}
|
| Since this method takes no arguments and
doesn't return a value it can also be executed on a foreground thread using a
ThreadStart delegate. |
System.Threading.ThreadStart ts = new
System.Threading.ThreadStart(LongRunningMethod);
System.Threading.Thread t = new System.Threading.Thread(ts);
t.Start();
|
| The last part is returning control to the main
UI thread to update the UI. This requires another delegate referencing an UI
updater method. The BeginInvoke method of the form is used to place the
delegate into the event pump to get called. |
protected void UpdateUI()
{
}
MethodInvoker updaterMI = new MethodInvoker(UpdateUI);
this.BeginInvoke(updaterMI);
|
| Using this code the LongRunningMethod becomes
the following. |
protected void LongRunningMethod()
{
int sleepTime = 10000;
int currentTime = sleepTime;
int step = 100;
while (currentTime > 0)
{
System.Threading.Thread.Sleep(step);
currentTime -= step;
}
MethodInvoker updaterMI = new MethodInvoker(UpdateUI);
this.BeginInvoke(updaterMI);
}
|
| The BeginInvoke is used as it releases the
worker thread immediately. |
| Protecting Data in a Mutithreaded
Environment |
| When in a multithreaded environment data must
be protected from multiple threads modifying the data simultaneously. While not
truly simultaneous, one thread could be in the middle of a modification when it
gets interrupted and another thread starts. This is called a race condition
because the first one wins. |
| .Net provides several options for
synchronizing or protecting data. Only using the Monitor class will be
discussed. The Monitor is used like the following where the item in the
parenthesis is the item to lock. |
lock(this)
{
//code to access the data goes here
}
|
| This statement causes the compiler to emit
code to use the Monitor class to protect the data. This will allow only a
single thread into the protected section at any time. It is important to design
applications that minimize synchronization needs. |
| Status Bar |
| The status bar is a common UI feature to
inform the user about long running processes. Since the UI should only be
updated by the UI thread the async thread needs a means of notifying the UI on
its progress. Events would appear to be the logical choice for communicating
the status. In order to safely pass data back and for the between threads a
reference type should be used. Since this is an event a new class derived from
EventArgs should be used. |
public class NotificationEventArgs :
EventArgs
{
private int percentComplete;
public int PercentComplete
{
get{ return percentComplete;}
set{ percentComplete = value;}
}
public NotificationEventArgs(int percentComplete)
{
this.percentComplete = percentComplete;
}
}
|
| The event handler delegate and method need to
be created. |
public delegate void UpdateStatusDelegate(
object sender, NotificationEventArgs e);
public void UpdateStatusHandler( object sender, NotificationEventArgs e)
{
progressBar1.Value = e.PercentComplete;
}
|
| The long running method can be changed to
allow for this update to occur. |
protected void LongRunningMethodUI()
{
int sleepTime = 10000;
int currentTime = sleepTime;
int step = 100;
UpdateStatusDelegate updateStatus = new
UpdateStatusDelegate(UpdateStatusHandler);
NotificationEventArgs e = new NotificationEventArgs(0);
Object sender = System.Threading.Thread.CurrentThread;
updateStatus(sender, e);
while (currentTime > 0)
{
System.Threading.Thread.Sleep(step);
currentTime -= step;
e.PercentComplete = 100 - (int)(((double)currentTime / (double)sleepTime)*
100);
updateStatus(sender, e);
}
}
|
| This solution still updates the UI on the
background thread. A solution is needed to get the event to run on the UI
thread. The Control class contains a solution to this issue. The InvokeRequired
determines if the call is coming from the current UI thread or from a separate
thread. The update method can be changed to incorporate this in the following
manner. |
protected void UpdateStatusHandler ( object sender, NotificationEventArs e)
{
if( this.InvokeRequired)
{
UpdateStatusDelegate updateStatus = new
UpdateStatusDelegate(UpdateStatusHandler);
this.Invoke(updateStatus, new object[]{sender, e});
}
else
{
progressBar1.Value = e.PercentComplete;
}
}
|
| This final variation will cause the event to
be placed on the UI thread for completion. |
| Summary |
Creating multithreaded applications in .Net
has been made fairly simple. Some care must be made in determining how to best
leverage this capability. Hopefully this brief summary will provide a brief
background for further investigation.
Download the code that accompanies this article |