Making a Rich Client Smart : Using Multiple Threads


Another TechEd demo. This time its how to use a ‘task’ pattern to manage asynchronous background tasks and web service requests. This is a variation on my previous article which describes a simplified asynchronous call pattern for Windows Forms applications, generalized a little and with a few interesting additions.


 


The Sample Application


The sample application is a Windows Forms application that performs a calculation that takes a while to complete. The calculation in this sample is simply a loop around a Thread.Sleep for 250 milliseconds. The calculation takes a single integer parameter which specifies the number of times around the loop. For the user interface, the application has a numeric up down control to specify the loop count, a button to start and stop the calculation, and a progress internal class CalculationTask


{


private CalculationStatus _calcState =


CalculationStatus.Completed;


 


// Define delegate for the actual calculation method.


private delegate void CalculationDelegate( int countTotal );


 


// Define the property changed events.


internal event CalculationStatusEventHandler


CalculationStatusChanged;


internal event CalculationProgressEventHandler


CalculationProgressChanged;


 


// Define the delegates for the property changed events.


internal delegate void CalculationStatusEventHandler(


object sender, CalculationStatusArgs e );


internal delegate void CalculationProgressEventHandler(


object sender, CalculationStatusArgs e );


 


// Methods.


internal void StartCalculation( int countTotal )


internal void StopCalculation()


 


private void FireStatusChangedEvent( CalculationStatus status )


 


private void FireProgressChangedEvent( int countTotal,


   int countCurrent )


private void Calculate( int countTotal )


private void EndCaclulation( IAsyncResult ar )


}


 


The class provides two externally accessible methods to start and stop the calculation, and two events which are used to communicate calculation progress and status changes. These events are fired on the UI thread if the event sink is a control derived object. These events are fired using the two helper methods.


 


The calculation status member is an enum which defines the four states of the calculation:


 


internal enum CalculationStatus


{


Completed,


Cancelled,


Calculating,


CancelPending


}


 


As noted above, the actual ‘calculation’ is a simple loop around a Thread.Sleep call.


 


private void Calculate( int countTotal )


{


// Initialize progress.


FireProgressChangedEvent( countTotal, 0 );


 


for( int i = 0; i < countTotal; i++ )


{


Thread.Sleep( 250 );


 


// Read operations are atomic.


if ( _calcState == CalculationStatus.CancelPending ) break;


 


// Update Progress


FireProgressChangedEvent( countTotal, i + 1 );


}


 


return;


}


 


This method is private to the CalculationTask class – after all this is the logic which we are encapsulating. The event firing helper methods allow the calculation progress to be reported to the parent Windows Form. The calculation state member is checked each time around the loop to see if the calculation has recently been cancelled by the user.


 


The following two methods are used to start and stop the calculation.


 


internal void StartCalculation( int countTotal )


{


lock( this )


{


if( _calcState == CalculationStatus.Completed ||


    _calcState == CalculationStatus.Cancelled )


{


// Create a delegate to the calculation method.


CalculationDelegate calculationMethod =


new CalculationDelegate( Calculate );


 


// Start the calculation.


calculationMethod.BeginInvoke( countTotal,


new AsyncCallback( EndCalculation ),


calculationMethod );


 


// Update the calculation status.


_calcState = CalculationStatus.Calculating;


 


// Fire a status changed event.


FireStatusChangedEvent( _calcState );


}


}


}


 


internal void StopCalculation()


{


lock( this )


{


if( _calcState == CalculationStatus.Calculating )


{


// Update the calculation status.


_calcState = CalculationStatus.CancelPending;


 


// Fire a status changed event.


FireStatusChangedEvent( _calcState );


}


}


}


 


The above code uses a delegate to invoke the calculation method asynchronously so the actual calculation will be performed using a thread from the thread pool. The code creates a delegate to the calculation method and an AsyncCallback delegate to the EndCalculation method so we can tidy up after the calculation thread has finished. Note the lock statements which synchronize access to the calculation status member to prevent a race condition. The EndCalculation method looks like this:


 


private void EndCalculation( IAsyncResult ar )


{


try


{


CalculationDelegate calculationMethod =


(CalculationDelegate)ar.AsyncState;


calculationMethod.EndInvoke( ar );


 


lock( this )


{


if ( _calcState == CalculationStatus.CancelPending )


{


_calcState = CalculationStatus.Cancelled;


}


else


{


_calcState = CalculationStatus.Completed;


}


FireStatusChangedEvent( _calcState );


}


}


catch( Exception ex )


{


}


}


 


This code retrieves the calculation delegate and calls the EndInvoke method to tidy up. The calculation status member is then updated and an appropriate status changed event fired. Any exceptions that are encountered are swallowed.


 


That just leaves the two helper methods which fire the actual events:


 


private void FireStatusChangedEvent( CalculationStatus status )


{


CalculationStatusEventHandler eventTarget =


CalculationStatusChanged;


 


if( eventTarget != null )


{


CalculationStatusArgs args =


new CalculationStatusArgs( status );


if ( eventTarget.Target is ISynchronizeInvoke )


{


ISynchronizeInvoke target = eventTarget.Target


as ISynchronizeInvoke;


target.BeginInvoke( eventTarget, new object[]


{ this, args } );


}


else


{


eventTarget( this, args );


}


}


}


 


private void FireProgressChangedEvent( int countTotal,


   int countCurrent )


{


CalculationProgressEventHandler eventTarget =


CalculationProgressChanged;


 


if( eventTarget != null )


{


CalculationStatusArgs args = new


CalculationStatusArgs( countTotal, countCurrent );


if ( eventTarget.Target is ISynchronizeInvoke )


{


ISynchronizeInvoke target = eventTarget.Target


as ISynchronizeInvoke;


target.BeginInvoke( eventTarget, new object[]


{ this, args } );


}


else


{


eventTarget( this, args );


}


}


}


 


Both of these methods check the type of the event target. If the target implements the ISynchronizeInvoke interface, then the event is fired using the BeginInvoke method on that interface. For System.Windows.Forms.Control derived objects this means that the event will be fired no the UI thread. For other targets, the event is fired in the normal way.


 


Both of these events define an argument which contains the details of the calculation status. The CalculationStatusArgs class is defined as follows:


 


internal class CalculationStatusArgs : EventArgs


{


public int               CountTotal;


public int               CountCurrent;


public CalculationStatus Status;


 


public CalculationStatusArgs( int countTotal, int countCurrent )


{


this.CountTotal   = countTotal;


this.CountCurrent = countCurrent;


this.Status       = CalculationStatus.Calculating;


}


 


public CalculationStatusArgs( CalculationStatus status )


{


this.Status = status;


}


}


 


This class provides two constructors so that the calculation status or progress details can be set.


 


The User Interface


So that’s the calculation task class defined. To use it in the sample application’s main form, we simply need to create an instance of it and subscribe to its events. The class which defines the main form looks like this:


 


public class TaskPatternForm : System.Windows.Forms.Form


{


// Controls…


 


// Other members.


private CalculationTask                   _calculationTask;


private CalculationTask.CalculationStatus _currentStatus;


 


public TaskPatternForm()


{


InitializeComponent();


 


// Create new task object to manage the calculation.


_calculationTask = new CalculationTask();


 


// Subscribe to the calculation status event.


_calculationTask.CalculationStatusChanged +=


new CalculationTask.CalculationStatusEventHandler(


OnCalculationStatusChanged );


 


// Subscribe to the calculation progress event.


_calculationTask.CalculationProgressChanged +=


                new CalculationTask.CalculationProgressEventHandler(


OnCalculationProgressChanged );


}


 


// Other methods…


}


 


The form class has two members (in addition to the UI control members). The class keeps track of the current status using an instance of the CalculationStatus enum, and it has a reference to an instance of the calculation task class itself. In the constructor of the form, the task class is created and the two event handlers hooked up. The event handlers look like this:


 


private void OnCalculationStatusChanged( object sender,


CalculationTask.CalculationStatusArgs e )


{


// Make sure we are on the UI thread.


Debug.Assert( !this.InvokeRequired );


 


// Make a note of the current status so we can update


// the Calculate/Cancel button.


_currentStatus = e.Status;


 


// Set the appropriate button state.


switch ( _currentStatus )


{


case CalculationTask.CalculationStatus.Completed:


calcButton.Text    = “Calculate”;


calcButton.Enabled = true;


progressBar1.Value = 0;


MessageBox.Show( “Calculation Completed!” );


break;


 


case CalculationTask.CalculationStatus.Cancelled:


calcButton.Text    = “Calculate”;


calcButton.Enabled = true;


MessageBox.Show( “Calculation Cancelled!” );


break;


 


case CalculationTask.CalculationStatus.Calculating:


calcButton.Text    = “Cancel”;


calcButton.Enabled = true;


break;


 


case CalculationTask.CalculationStatus.CancelPending:


calcButton.Text    = “Canceling…”;


calcButton.Enabled = false;


break;


}


}


 


private void OnCalculationProgressChanged( object sender,


CalculationTask.CalculationStatusArgs e )


{


// Make sure we are on the UI thread.


Debug.Assert( !this.InvokeRequired );


 


// Update the progress progressBar1.Maximum = e.CountTotal;


progressBar1.Value   = e.CountCurrent;


}


 


In this sample a single button is used to start and stop the calculation. When the status changed event is fired, the calculation status is updated and the state of the button is changed accordingly. If the calculation has been completed, then the button is enabled and its text set appropriately so that the user can start another calculation. If the calculation has been started or is in the process of being cancelled, the button text is updated and the button is disabled to show the user what is happening.


 


The actual event handler for the button on the form looks like this:


 


void CalculateButtonClick( object sender, System.EventArgs e )


{


// Make sure we are on the UI thread.


Debug.Assert( this.InvokeRequired == false );


 


// Start or cancel the calculation.


switch ( _currentStatus )


{


case CalculationTask.CalculationStatus.Completed:


case CalculationTask.CalculationStatus.Cancelled:


_calculationTask.StartCalculation(


(int)countUpDown.Value );


break;


 


case CalculationTask.CalculationStatus.Calculating:


_calculationTask.StopCalculation();


break;


}


}


 


Depending on the calculation status, the calculation is simply started or stopped when the user presses the button.


 


Note that all event handlers check that the calling thread is in fact the UI thread. If any of these events are fired on another thread, the assert will kick in and give an error message.


Conclusion


The task pattern is a simple but effective way to structure your application so that the details of the background task are encapsulated away from the user interface. The user interface classes should never have to worry about threads – they should be focused on making sure that the user interface is up to date and always responsive.


 


The next version of the .NET Framework (Whidbey) provides a background worker class which allows you to specify a background task and which fires events on the UI thread. The background worker class is similar to that provided above, though it does not provide quite the same level of encapsulation.


 


 


Copyright © David Hill, 2004.
THIS CODE AND INFORMATION ARE PROVIDED “AS IS” WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE.

Comments (2)

  1. P. Kim says:

    Thank you for the excellent article; this is one of the cleanest design patterns for asynchronous tasks that I have encountered. However, is it not the case that the Target property of the event returns only the target of the last instance method on the event’s invocation list? If there are multiple threads subscribed to the event, therefore, it is not guaranteed that the event will be fired in a thread-safe manner for all clients, and responsibility for thread safety again devolves to the user interface.

  2. David Hill says:

    The Control.Invoke or BeginInvoke methods ensure that the target of the delegate receives the event on the UI thread. If the delegate happens to be a multicast delegate (ie, a delegate chain) then all targets will be called on the UI thread.

    It won’t matter if the other targets are not Control derived objects. The event will be invoked on the thread of the control on which the Invoke or BeginInvoke method was originally called. In this case, the target will probably not care which thread it is called on because it is not Control derived.

    Of course, if you needed more control over whether each event was marshalled to the UI thread or not, you could always use the GetInvocationList method of the delegate and fire each one manually, checking for the target’s type and handling exception appropriately, etc. You would also have to do this if you had multiple message loops, each on a different thread with controls created on each thread.

Skip to main content