C# Corner
Asynchronous Programming in .NET: I'll Call You Back
VSM Web columnist Eric Vogel kicks off his first C# Corner installment with a walk through creating an application using asynchronous programming.
Asynchronous programming is a means of parallel programming in which a unit of work runs separately from the main application thread and notifies the calling thread of its completion, failure or progress. You may be wondering when you should use asynchronous programming and what are its benefits and problem points.
The main benefits one can gain from using asynchronous programming are improved application performance and responsiveness. One particularly well suited application for the asynchronous pattern is providing a responsive UI in a client application while running a computationally or resource expensive operation. Today we will get started with the basics so you can learn how to become an asynchronous programming ninja and round house kick your multi-threading woes.
The .NET Framework provides a few avenues to get on the ramp to asynchronous programming. Some of your implementation choices from the most basic to complex include using background workers, invoking a method asynchronously from a delegate, or implementing the IAsynchResult interface. All of these options allow you to multi-thread your application without ever having to manage your own threads. The .NET Framework asynchronous APIs handle this drudgery for you.
Setting up our Application
What better way to learn asynchronous programming than to create an application? Our sample application will compute the factorial of a number while maintaining a responsive user interface. The user will be displayed a status message of "Calculating" while the factorial is being calculated. When the factorial has finished being calculated, the status message will be updated to "Completed" and the result will be updated.
[Click on image for larger view.] |
Figure 1. |
Open up Visual Studio and create a new WPF C# Application. Open up MainWindow.xaml and place the markup below in the Window element content.
<Grid>
<StackPanel>
<StackPanel Orientation="Horizontal">
<Label>Number:</Label>
<TextBox Name="txtArg" MinWidth="100"></TextBox>
</StackPanel>
<StackPanel Orientation="Horizontal">
<Label Name="lblCompute">Result:</Label>
<Label Name="lblResult"></Label>
</StackPanel>
<StackPanel Orientation="Horizontal">
<Label>Status</Label>
<Label Name="lblStatus"></Label>
</StackPanel>
<Button Name="btnCompute" Click="btnCompute_Click">Compute</Button>
</StackPanel>
</Grid>
Next create a new C# Library project and name it Common. Add a new class named Math to the Common library. The Math class will contain our Factorial function that will be used by all of the examples. The factorial of a number (n!) is defined as the product of n with all positive integers less than n. The factorial of 0 is defined as 1.
public static ulong Factorial(ulong number)
{
System.Threading.Thread.Sleep(25);
if (number == 0)
{
return 1;
}
return number * Factorial(number - 1);
}
Now on to the fun part of wiring up our application to call the Factorial function asynchronously.
Creating a Background Worker
The most basic asynchronous component available in .NET is the background worker. It is also the easiest to use and is well suited for many problems. The background worker component allows you to start, update and cancel an asynchronous operation. When the user clicks on the Computer button we will initiate our call to the factorial function and set up a callback function. While the task is being performed we will also notify the user that the action is pending. All the while the task is being processed, the user may still interact with the application.
Open up your MainWindow.xaml.cs file.
First of all include the System.ComponentModel namespace.
using System.ComponentModel;
Next construct your background worker component.
BackgroundWorker _bw = new BackgroundWorker();
Next we will wire up our DoWork and RunWorkerCompleted event of the background worker with our long running function. The DoWork event will be fired when RunWorkerSync method is called on the background worker. The RunWorkerCompleted event will be fired when the background worker's DoWork event finishes.
_bw.DoWork += new DoWorkEventHandler(bw_DoWork);
_bw.RunWorkerCompleted += new RunWorkerCompletedEventHandler(_bw_RunWorkerCompleted);
void bw_DoWork(object sender, DoWorkEventArgs e)
{
long number = (long)e.Argument;
e.Result = Factorial(number);
}
Next we instruct the background worker to run our computation in our button's click event and display the "Calulating... " status message.
private void btnCompute_Click(object sender, RoutedEventArgs e)
{
long arg = long.Parse(txtArg.Text);
lblStatus.Content = "Calculating...";
_bw.RunWorkerAsync(arg);
}
Finally we update out result label in the background worker's RunWorkerCompleted event.
void _bw_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
lblResult.Content = e.Result;
lblStatus.Content = "Completed";
}
Calling a method asynchronously from a delegate
Our next available option in .NET is to implement our own asynchronous events. Let's go over how to implement the previous example using an asynchronous delegate in place of the background worker component. First create a delegate that will return a long and take in a long parameter.
Func<long, long> _factorialFunc;
Next assign the delegate to the Factorial function.
_factorialFunc = Factorial;
Finally we invoke the delegate in the button's click event, passing in a callback function that will be called when the asynchronous function completes.
private void btnCompute_Click(object sender, RoutedEventArgs e)
{
long arg = long.Parse(txtArg.Text);
lblStatus.Content = "Calculating...";
_factorialFunc.BeginInvoke(arg, FactorialCompleted, null);
}
void FactorialCompleted(IAsyncResult asynchResult)
{
Func<ulong, ulong> target = (Func<ulong, ulong>)asynchResult.AsyncState;
ulong result = target.EndInvoke(asynchResult);
Dispatcher.BeginInvoke(new Action<ulong>(UpdateResult), result);
Dispatcher.BeginInvoke(new Action<string>(UpdateStatus), "Completed");
}
void UpdateResult(ulong result)
{
lblResult.Content = result;
}
void UpdateStatus(string status)
{
lblStatus.Content = status;
}
The FactorialCompleted callback function retrieves the factorial result from the delegate and sets the result and status labels on the form.
Implementing the IAsynchResult Interface
Our third and most complex asynchronous programming option is to implement the IAsychResult interface and create BeginX and EndX versions of your operation. The BeginX and EndX operations should perform the operation asynchronously. The IAsynchResult interface includes an optional AysncState property that may contain information about the operation being performed. The AsynchWaitHandle property is used to have the operation run synchronously. The CompletedSynchronously property should be set to true if the operation completed on the same thread it was called upon. The IsCompleted property should be set to true if the operation has finished running.
Let's start out with defining our own implementation of IAsynchResult, which will be utilized by our BeginFactorial and EndFactorial functions. Our implementation of IAsynchResult will be generic and accept an argument and a result of specified types. The code is displayed in Listing 1 below.
Click to view Listing 1.
The BeginFactorial method executes the FactorialAsynch method and passes it the asynchronous object state. The FactorialAsynch method invokes our Factorial method with the argument that is set in the AsynchResult object and calls the SetAsCompleted method once the Factorial function has completed. Additionally, we handle any exception that might occur and pass that to the AsynchResult to handle. The EndFactorial function simply invokes EndInvoke on AsychResult to wait for the function to complete if it is still pending. Finally we update our client application to utilize the BeginFactorial and EndFactorial functions.
private void btnCompute_Click(object sender, RoutedEventArgs e)
{
Common.Math mathLib = new Common.Math();
ulong number = ulong.Parse(txtArg.Text);
lblStatus.Content = "Calculating...";
IAsyncResult asynchResult = mathLib.BeginFactorial(number, FactorialCallBack, mathLib);
}
private void FactorialCallBack(IAsyncResult asynchResult)
{
Common.Math target = (Common.Math)asynchResult.AsyncState;
ulong result = target.EndFactorial(asynchResult);
Dispatcher.BeginInvoke(new Action<ulong>(UpdateResult), result);
Dispatcher.BeginInvoke(new Action<string>(UpdateStatus), "Completed");
}
void UpdateResult(ulong result)
{
lblResult.Content = result;
}
void UpdateStatus(string status)
{
lblStatus.Content = status;
}
The code is very similar to the asynchronous event example; we call our BeginFactorial function in the computer button click event passing our argument along with a callback and the object state. In the callback function we retrieve the result via the EndFactorial method and update our UI to display the result along with a Completed message.
Conclusion
As you can see asynchronous programming has many benefits but does add complexity to your code. Like all programming patterns it is best to weigh the benefits against the added complexity to see if it is the right fit for your application.
I recommend starting out with the background worker and asynchronous event methods first. In most cases you will not need to implement your own version of IAsynchResult, unless you are running into performance issues with one of the other methods or have another API that is dependent on IAsynchResult.
Reference
Richter, Jeffrey. "Implementing the CLR Asynchronous Programming Model." MSDN | Microsoft Development, Subscriptions, Resources, and More. MSDN, Mar. 2007. Web. 22 Mar. 2011. .
The AsychResult<TArg,TResult> in the listing was inspired by and based upon the AsychResult<TResult> in the referenced article.
About the Author
Eric Vogel is a Senior Software Developer for Red Cedar Solutions Group in Okemos, Michigan. He is the president of the Greater Lansing User Group for .NET. Eric enjoys learning about software architecture and craftsmanship, and is always looking for ways to create more robust and testable applications. Contact him at [email protected].