Sunday, September 26, 2004

.NET: Solution-pattern for long-running UI responsive applications

Many a time we face this problem of updating the UI while the worker/IO thread is still performing some time consuming background action, fetching results and trying to change the UI while the UI also needs to be "freeze-free" and responsive to a user. Sometimes the UI also needs to support a "Cancel/Close" operation.

There are various solutions to this problem in .NET Winforms -- the most commonly used one is the lock mechanism - so as to enable the worker thread to safely update the UI data.

But there is a much simpler scalable solution using message passing, as .NET has 2 nice features -- 1) in which a control can tell us whether the caller of a function is a UI thread(control creator thread) or not by using the InvokeRequired property. If the caller is not a UI Thread obviously we will need to make calls to the control through the Invoke method, which is a synchronous method of a control to execute a function on the thread that owns the control's underlying window handle(UI thread), and the feature 2) in which any delegate(function) can be called asynchronously [using a generated worker thread (from the .NET thread pool) to execute the function asynchronously] by using the BeginInvoke function.

The following code is a general pattern to solve the above problem based on an example in .NET guru Chris Sells book "Windows Forms Programming in C#". I'd like to thank the author for his example and insight into this problem solution.

Platforms
Tested on .NET 1.1/2.0 and Windows NT, 98, 2000, XP, 2003

Type of Sample:
Create a new WinForms Project in VS.NET.

Main Components of the example:
Drag the following components from the toolbar on to the form

ProgressBar opProgress; //a progressbar indicating job progress
Button longOpButton;//button to start/cancel the operation
TextBox resultsBox;// a text box - read only - for scrolling through results
Label maxOpsLabel;// a label to indicate the operations complete till now


and add the following constant for the num of times to repeat the operation

///


/// This is the maximum amount of times the job will execute
/// Change this if you want better control over the operation.
///

const int MaxDigits = 10000;

Enum for the Operation States

///
/// The states of the Long Operation
///

enum OpState
{
Pending, // No Long worker operation running or canceling
InProgress, // Long worker operation in progress
Canceled, // Long worker operation canceled in UI but not worker
}


OpState state = OpState.Pending; //initial state

Custom EventArgs to be passed to the ShowProgress Handler
/// 

/// class to hold custom Progress event arguments
///

class ShowProgressArgs : EventArgs
{
public string results;
public int totalDigits;
public int digitsSoFar;
//should the operation be cancelled
public bool cancel;

public ShowProgressArgs(string results, int totalDigits, int digitsSoFar)
{
this.results = results;
this.totalDigits = totalDigits;
this.digitsSoFar = digitsSoFar;
}
}

ShowProgress delegate and function used to display progress
//delegate that takes a sender and an instance on the custom arguments object.

delegate void ShowProgressHandler(object sender, ShowProgressArgs e);
/// ShowProgress makes sure the UI thread will handle UI changes(progress updates,etc)

/// If ShowProgress is called from the UI thread,
/// it will update the controls, but if it's called from a worker
/// thread, it uses BeginInvoke to call itself back on the
/// UI thread.
void ShowProgress(object sender, ShowProgressArgs e)
{
// Make sure we're on the UI thread
if( this.InvokeRequired == false )
{
resultsBox.Text = e.results;
opProgress.Maximum = e.totalDigits;
opProgress.Value = e.digitsSoFar;
this.maxOpsLabel.Text = e.digitsSoFar.ToString();

Application.DoEvents();
// Check for Cancel
e.cancel = (state == OpState.Canceled);

// Check for completion
if( e.cancel (e.digitsSoFar == e.totalDigits) )
{
state = OpState.Pending;
longOpButton.Text = "Calc";
longOpButton.Enabled = true;
}
}
// Transfer control to the UI thread
else
{
//send message to UI thread synchronously
Invoke(new ShowProgressHandler(ShowProgress), new object[] { sender, e });
}
}

PerformJob delegate and function which is the Long operation
/// 

/// delegate to call the Long operation asynchronously
///

delegate void PerformJobDelegate(int digits);
/// 

/// The heart of the long operation
/// can be any kind of worker thread intensive operation.
/// Here 9 digits are sent to the display at a time
/// till the maxdigits are reached
///

void PerformJob(int digits)
{
StringBuilder calcResult = new StringBuilder("", digits + 2);
object sender = System.Threading.Thread.CurrentThread;
ShowProgressArgs e = new ShowProgressArgs(calcResult.ToString(), digits, 0);
// Show progress
ShowProgress(sender, e);

if( digits > 0 )
{
const string nineDigitsString="123456789-";
for( int i = 0; i < digits; i += 9 )
{
calcResult.Append(nineDigitsString);

// Show progress
e.results = calcResult.ToString();
e.digitsSoFar = i + 9;
ShowProgress(sender, e);
// check for Cancel
if( e.cancel ) break;
}
}
}

The Operation Start/Cancel Button Click Event handler
/// 

/// This technique represents a message passing model.
/// This model is clear, safe, general-purpose, and scalable.
/// It's clear because it's easy to see that the worker is creating a message,
/// passing it to the UI, and then checking the message for information that may
/// have been added during the UI thread's processing of the message.
/// It's safe because the ownership of the message is never shared,
/// starting with the worker thread, moving to the UI thread, and then
/// returning to the worker thread, with no simultaneous access between
/// the two threads. It's general-purpose because if the worker or UI
/// thread needed to communicate information in addition to a cancel flag,
/// that information can be added to the ShowProgressArgs class.
/// Finally, this technique is scalable because it uses a thread pool,
/// which can handle a large number of long-running operations more
/// efficiently than naively creating a new thread for each one.
/// For long-running operations in your WinForms applications,
/// you should first consider message passing.
///

private void PerformOpbtn_Click(object sender, System.EventArgs e)
{
// Calc button does double duty as Cancel button
switch( state )
{
// Start a new Long worker operation
case OpState.Pending:
// Allow canceling
state = OpState.InProgress;
longOpButton.Text = "Cancel";

// Async delegate method
PerformJobDelegate PerformOp = new PerformJobDelegate(this.PerformJob);
//Perform the Long Operation MaxDigits times
PerformOp.BeginInvoke(MaxDigits, null, null);
break;

// Cancel a running Long worker operation
case OpState.InProgress:
state = OpState.Canceled;
longOpButton.Enabled = false;
break;

// Shouldn't be able to press Calc button while it's canceling
case OpState.Canceled:
Debug.Assert(false);
break;
}
}
Closing Notes:-
  • Please handle the Form Closing Event
    /// 
    
    /// Handle the closing event of this LongOpUI form
    ///

    private void LongOpUI_Closing(object sender, System.ComponentModel.CancelEventArgs e)
    {
    //send a cancel signal
    if (this.state == OpState.InProgress)
    this.longOpButton.PerformClick();
    }

  • Strategy for waiting till the operation completes
    //"EndInvoke does not return until the asynchronous call completes.
    
    //This is a good technique to use with file or network operations,
    //but because it blocks on EndInvoke, you should not use it from threads
    //that service the user interface.Waiting on a WaitHandle is a common thread
    //synchronization technique. You can obtain a WaitHandle using the AsyncWaitHandle
    //property of the IAsyncResult returned by BeginInvoke. The WaitHandle is signaled
    //when the asynchronous call completes, and you can wait for it by calling its WaitOne."
    //-- Source MSDN
    IAsyncResult aResult = PerformOp.BeginInvoke(MaxDigits, null, null);
    //Wait for the call to complete
    aResult.AsyncWaitHandle.WaitOne();
    callResult = PerformOp.EndInvoke(aResult);
    MessageBox.Show("Result of calling PerformJob with " + MaxDigits + " is " + callResult);
  • Polling for Asynchronous Call Completion
    You can use the IsCompleted property of the IAsyncResult returned by BeginInvoke
    
    to discover when the asynchronous call completes. You might do this when making
    the asynchronous call from a thread that services the user interface.
    Polling for completion allows the user interface thread to continue processing
    user input.
  • Need for a custom ThreadPool
    The .NET ThreadPool has a default limit of 25 threads per available processor.
    
    Change this setting in machine.config, but still your app may land in
    "threadpool starvation" issues as the threadpool is used for almost all
    async callbacks. Timer-queue timers and registered wait operations also use
    the thread pool. Their callback functions are queued to the ThreadPool.
    You can also Queue Work Items to the ThreadPool. Also ASP.NET WebRequests
    are serviced by the ThreadPool.
    So, sometimes you may need to write your own custom ThreadPool to solve
    these issues with the "free" .NET ThreadPool,
    see a sample at http://www.thecodeproject.com/csharp/SmartThreadPool.asp
    Thanks to the Author Ami Bar for that article.


No comments: