Core C# and .NET
< Day Day Up > |
In a synchronous (single-threaded) application, program execution follows a single path; in an asynchronous (multithreaded) version, operations occur in parallel on multiple paths of execution. This advantage of this latter approach is that slow applications, such as file I/O, can be performed on a separate thread while the main thread continues execution. Figure 13-3 provides an abstract representation of the two techniques. In the synchronous version, each method is executed in sequence; in the asynchronous version, method B runs at the same time as A and C. This prospect of two or more tasks running (nearly) simultaneously raises a set of questions not present in a single-threaded program:
Figure 13-3. Synchronous versus asynchronous execution
Before tackling these issues, let's first look at the basics of how to write code that provides asynchronous code execution. As we see in the next section, threads can be explicitly created and used for parallel code execution. An easier approach is to use a delegate to allocate a worker thread and call a method to execute on the thread a process referred to as asynchronous delegate invocation. Delegates can also be used to specify the callback method that a worker thread calls when it finishes execution. Although a discussion of creating threads is deferred until later in this chapter, it's worth noting now that the threads allocated for asynchronous methods come from a pre-allocated thread pool. This eliminates the overhead of dynamically creating threads and also means they can be reused. At the same time, indiscriminate use of asynchronous calls can exhaust the thread pool causing operations to wait until new threads are available. We'll discuss remedies for this in the section on threads. Asynchronous Delegates
Delegates which were introduced in Chapter 4, "Working with Objects in C#" provide a way to notify one or more subscribing methods when an event occurs. In the earlier examples, all calls were synchronous (to methods on the same thread). But delegates can also be used to make an asynchronous call that invokes a method on a separate worker thread. Before looking at the details of this, let's review what a delegate is and how it's used. The following code segment illustrates the basic steps involved in declaring a delegate and using it to invoke a subscribing method. The key points to note are that the callback method(s) must have the same signature as the delegate's declaration, and that multiple methods can be placed on the delegate's invocation chain (list of methods to call). In this example, the delegate is defined to accept a string parameter and return no value. ShowUpper and ShowMessage have the same signature. //(1) Declare delegate. Declare anywhere a class can be declared. public delegate void myDelegate(string msg); private void TestDelegate() { // (2) Create instance of delegate and pass method to it myDelegate msgDelegate= new myDelegate(ShowMessage); // Second method is placed on delegate invocation chain msgDelegate+= new myDelegate(ShowUpper); // (3) Invoke delegate msgDelegate("Delegate Called."); } // First method called by delegate private void ShowMessage(string msg) { MessageBox.Show(msg); } // Second method called by delegate private void ShowUpper(string msg) { msg = msg.ToUpper(); // Make uppercase before displaying MessageBox.Show(msg); } Understanding the Delegate Class
When a delegate is defined, .NET automatically creates a class to represent the delegate. Here is the code generated for the delegate in the preceding example: // Class created from delegate declaration public class myDelegate : MulticastDelegate { // Constructor public myDelegate(Object target, Int32 methodPtr); public void virtual Invoke(string msg); // Used for asynchronous invocation public virtual IAsyncResult BeginInvoke( string msg, AsyncCallback callback, Object state); // Used to get results from called method public virtual void EndInvoke(IAsyncResult result); // Other members are not shown } A close look at the code reveals how delegates support both synchronous and asynchronous calls. Constructor
Takes two parameters. The important thing to note here is that when your program creates an instance of the delegate, it passes a method name to the constructor not two parameters. The compiler takes care of the details of generating the parameters from the method name. Invoke
The compiler generates a call to this method by default when a delegate is invoked. This causes all methods in the invocation list to be called synchronously. Execution on the caller's thread is blocked until all of the methods in the list have executed. BeginInvoke
This is the method that enables a delegate to support asynchronous calls. Invoking it causes the delegate to call its registered method on a separate worker thread. BeginInvoke has two required parameters: the first is an AsyncCallback delegate that specifies the method to be called when the asynchronous method has completed its work; the second contains a value that is passed to the delegate when the method finishes executing. Both of these values are set to null if no callback is required. Any parameters defined in the delegate's signature precede these required parameters. Let's look at the simplest form of BeginInvoke first, where no callback delegate is provided. Here is the code to invoke the delegate defined in the preceding example asynchronously: IAsyncResult IAsync = msgDelegate.BeginInvoke("Delegate Called.",null,null)
There is one small problem, however this delegate has two methods registered with it and delegates invoked asynchronously can have only one. An attempt to compile this fails. The solution is to register only ShowMessage or ShowUpper with the delegate. Note that BeginInvoke returns an object that implements the IAsyncResult interface. As we see later, this object has two important purposes: It is used to retrieve the output generated by the asynchronous method; and its IsCompleted property can be used to monitor the status of the asynchronous operation. You can also pass an AsyncCallBack delegate as a parameter to BeginInvoke that specifies a callback method the asynchronous method invokes when its execution ends. This enables the calling thread to continue its tasks without continually polling the worker thread to determine if it has finished. In this code segment, myCallBack is called when ShowMessage finishes. private delegate void myDelegate(string msg); myDelegate d= new myDelegate(ShowMessage); d.BeginInvoke("OK",new AsyncCallback(myCallBack),null);
It is important to be aware that myCallBack is run on a thread from the thread pool rather than the application's main thread. As we will see, this affects the design of UI (user interface) applications. EndInvoke
Is called to retrieve the results returned by the asynchronous method. The method is called by passing it an object that implements the IAsyncResult interface the same object returned when BeginInvoke is called. These two statements illustrate this approach: // Save the interface returned IAsyncResult IAsync = GetStatus.BeginInvoke(null,null); // ... Do some work here; then get returned value int status = GetStatus.EndInvoke(IAsync);
EndInvoke should be called even if the asynchronous method returns no value. It can be used to detect exceptions that may be thrown by the asynchronous method; and more importantly, it notifies the Common Language Runtime (CLR) to clean up resources that were used in creating the asynchronous call. Examples of Implementing Asynchronous Calls
The challenge in using BeginInvoke is to determine when the called asynchronous method finishes executing. As touched on earlier, the .NET Framework offers several options:
Figure 13-4 illustrates the four options. Figure 13-4. Options for detecting the completion of an asynchronous task
Using Polling and Synchronization Objects
Table 13-1 lists the IAsyncResult properties that are instrumental in implementing the various asynchronous models. The class is in the System.Runtime.Remoting.Messaging namespace.
The WaitHandle and IsCompleted properties are often used together to implement polling logic that checks whether a method has finished running. Listing 13-1 illustrates this cooperation. A polling loop is set up that runs until IsCompleted is true. Inside the loop, some work is performed and the WaitHandle.WaitOne method is called to detect if the asynchronous method is done. WaitOne blocks processing until it receives a signal or its specified wait time (20 milliseconds in this example) expires. Listing 13-1. Asynchronous Invocation Using Polling to Check Status
// Code to return a Body Mass Index Value private delegate decimal bmiDelegate(decimal ht, decimal wt); decimal ht_in = 72; decimal wt_lbs=168; // (1) Invoke delegate asynchronously bmiDelegate bd= new bmiDelegate(CalcBMI); IAsyncResult asRes= bd.BeginInvoke(ht_in, wt_lbs,null,null); int numPolls=0; while(!asRes.IsCompleted) { // Do some work here // (2) Wait 20 milliseconds for method to signal completion asRes.AsyncWaitHandle.WaitOne(20,false); numPolls+=1; } // (3) Get result now that asynchronous method has finished decimal myBMI = bd.EndInvoke(asRes); Console.WriteLine("Polls: {0} BMI: {1:##.00}", numPolls, myBMI); // --> Polls: 3 BMI: 22.78 // Calculate BMI private decimal CalcBMI(decimal ht, decimal wt) { Thread.Sleep(200); // Simulate a delay of 200 ms Console.WriteLine("Thread:{0}", Thread.CurrentThread.GetHash()); return((wt * 703 *10/(ht*ht))/10); } For demonstration purposes, this example includes a 200-millisecond delay in the asynchronous method CalcBMI. This causes WaitOne, which blocks for up to 20 milliseconds, to execute seven times (occasionally eight) before the loop ends. Because EndInvoke is not reached until the asynchronous calculation has ended, it causes no blocking. A more interesting use of the WaitHandle methods is to manage multiple asynchronous tasks running concurrently. In this example, the static WaitAll method is used to ensure that three asynchronous tasks have completed before the results are retrieved. The method is executed by passing it an array that contains the wait handle created by each call to BeginInvoke. As a side note, this point where threads must rendezvous before execution can proceed is referred to as a barrier. int istart= Environment.TickCount; // Start Time bmiDelegate bd1 = new bmiDelegate(Form1.CalcBMI); IAsyncResult asRes1 = bd1.BeginInvoke(72, 168,null,null); // bmiDelegate bd2 = new bmiDelegate(CalcBMI); IAsyncResult asRes2 = bd2.BeginInvoke(62, 124,null,null); // bmiDelegate bd3 = new bmiDelegate(CalcBMI); IAsyncResult asRes3 = bd3.BeginInvoke(67, 132,null,null); // Set up array of wait handles as required by WaitAll method WaitHandle[] bmiHandles = {asRes1.AsyncWaitHandle, asRes2.AsyncWaitHandle, asRes3.AsyncWaitHandle); // Block execution until all threads finish at this barrier point WaitHandle.WaitAll(bmiHandles); int iend = Environment.TickCount; // Print time required to execute all asynchronous tasks Console.WriteLine("Elapsed Time: {0}", iend istart); // Get results decimal myBMI1 = bd1.EndInvoke(asRes1); decimal myBMI2 = bd2.EndInvoke(asRes2); decimal myBMI3 = bd3.EndInvoke(asRes3);
To test performance, the method containing this code was executed multiple times during a single session. The results showed that execution time was more than 700 milliseconds for the first execution and declined to 203 for the fourth and subsequent ones when three different threads were allocated. Execution: 1 2 3 4 5 Thread: 75 75 80 75 75 Thread: 75 80 12 80 80 Thread: 80 75 80 12 12 Time(ms): 750 578 406 203 203 For comparison, the code was then run to execute the three tasks with each BeginInvoke followed by an EndInvoke. It ran at a consistent 610 ms, which is what would be expected given the 200 ms block by each EndInvoke and is equivalent to using synchronous code. The lesson to a developer is that asynchronous code should be used when a method will be executed frequently; otherwise the overhead to set up multithreading negates the benefits. Core Note
Using Callbacks
Callbacks provide a way for a calling method to launch an asynchronous task and have it call a specified method when it is done. This is not only an intuitively appealing model, but is usually the most efficient asynchronous model permitting the calling thread to focus on its own processing rather than waiting for an activity to end. As a rule, the callback approach is preferred when the program is event driven; polling and waiting are better suited for applications that operate in a more algorithmic, deterministic manner. The next-to-last parameter passed to BeginInvoke is an optional delegate of type AsyncCallback. The method name passed to this delegate is the callback method that an asynchronous task calls when it finishes executing a method. The example in Listing 13-2 should clarify these details. Listing 13-2. Using a Callback Method with Asynchronous Calls
using System.Runtime.Remoting.Messaging ; // Delegate is defined globally for class public delegate decimal bmiDelegate(decimal wt, decimal ht); public class BMIExample { public void BMICaller(decimal ht, decimal wt, string name) { bmiDelegate bd= new bmiDelegate(CalcBMI); // Pass callback method and state value bd.BeginInvoke(ht,wt,new AsyncCallback(OnCallBack),name); } // This method is invoked when CalcBMI ends private void OnCallBack(IAsyncResult asResult) { // Need AsyncResult so we can get original delegate AsyncResult asyncObj = (AsyncResult)asResult; // Get state value string name= (string)asyncObj.AsyncState ; // Get original delegate so EndInvoke can be called bmiDelegate bd= (bmiDelegate)asyncObj.AsyncDelegate; // Always include exception handling try { decimal bmi = bd.EndInvoke(asResult); Console.WriteLine("BMI for {0}: {1:##.00}",name,bmi); } catch (Exception ex) { Console.WriteLine(ex.Message); } } private decimal CalcBMI(decimal ht, decimal wt) { Console.WriteLine("Thread:{0}", Thread.CurrentThread.GetHashCode()); return((wt * 703 *10/(ht*ht))/10); } }
Things to note:
Multiple Threads and User Interface Controls
When working with Windows Forms and user interfaces in general, it is important to understand that all controls on a form belong to the same thread and should be accessed only by code running on that thread. If multiple threads are running, a control should not be accessed even though it's technically accessible by any code not running on the same thread as the control. This is a .NET commandment; and as is the nature of commandments, it can be broken but with unpredictable results. Suppose our application wants to use the callback method in the preceding example to display the calculated BMI value on a label control. One's instinct might be to assign the value directly to the control: private void OnCallBack(IAsyncResult asResult) { // ... Initialization code goes here decimal bmi = bd.EndInvoke(asResult); Label.Text= bmi.ToText(); // Set label on UI to BMI value } This may work temporarily, but should be avoided. As an alternative, .NET permits a limited number of methods on the Control class to be called from other threads: Invoke, BeginInvoke, EndInvoke, and CreateGraphics. Calling a control's Invoke or BeginInvoke method causes the method specified in the delegate parameter to be executed on the UI thread of that control. The method can then work directly with the control. To illustrate, let's replace the assignment to Label.Text with a call to a method DisplayBMI that sets the label value: DisplayBMI(bmi);
We also add a new delegate, which is passed to Invoke, that has a parameter to hold the calculated value. // Delegate to pass BMI value to method private delegate void labelDelegate(decimal bmi); private void DisplayBMI(decimal bmi) { // Determines if the current thread is the same thread // the Form was created on. if(this.InvokeRequired == false) { labelthread.Text= bmi.ToString("##.00"); } else { // The Form's Invoke method is executed, which // causes DisplayBMI to run on the UI thread. // bmiObj is array of arguments to pass to method. object[] bmiObj= {bmi}; this.Invoke(new labelDelegate(DisplayBMI),bmiObj); } }
This code segment illustrates an important point about threads and code: The same code can be run on multiple threads. The first time this method is called, it runs on the same thread as OnCallBack. The InvokeRequired property is used to determine if the current thread can access the form. If not, the Invoke method is executed with a delegate that calls back DisplayBMI on the UI thread permitting it to now interact with the UI controls. To make this an asynchronous call, you only need replace Invoke with BeginInvoke. Using MethodInvoker to Create a Thread
In situations where your code needs to create a new thread but does not require passing arguments or receiving a return value, the system-defined MethodInvoker delegate should be considered. It is the simplest possible delegate it takes no parameters and returns no value. It is created by passing the name of a method to be called to its constructor. It may then be invoked synchronously (Invoke) or asynchronously (BeginInvoke): // NewThread is method called by delegate MethodInvoker mi = new MethodInvoker(NewThread); // Note that parameters do not have to be null mi.BeginInvoke(null,null); // Asynchronous call mi(); // Synchronous call
The advantage of using the built-in delegate is that you do not have to design your own, and it runs more efficiently than an equivalent custom delegate. Using Asynchronous Calls to Perform I/O
Asynchronous operations are not new; they were originally implemented in operating systems via hardware and software as a way to balance the slow I/O (Input/Output) process against the much faster CPU operations. To encourage asynchronous I/O, the .NET Framework includes methods on its major I/O classes that can be used to implement the asynchronous model without explicitly creating delegates or threads. These classes include FileStream, HttpWebRequest, Socket, and NetworkStream. Let's look at an example using the FileStream class that was introduced in Chapter 5, "C# Text Manipulation and File I/O." FileStream inherits from the System.IO.Stream class an abstract class that supports asynchronous operations with its BeginRead, BeginWrite, EndRead, and EndWrite methods. The Beginxxx methods are analogous to BeginInvoke and include callback and status parameters; the Endxxx methods provide blocking until a corresponding Beginxxx method finishes. The code in Listing 13-3 uses BeginRead to create a thread that reads a file and passes control to a callback method that compresses the file content and writes it as a .gz file. The basic callback method operations are similar to those in Listing 13-2. Note how the file name is retrieved from the AsyncState property. The compression technique based on the GZipStream class is available only in .NET 2.0 and above. Listing 13-3. Using Aysnchronous I/O to Compress a File
// Special namespaces required: using System.IO.Compression; using System.Runtime.Remoting.Messaging; // // Variables with class scope Byte[] buffer; FileStream infile; // Compress a specified file using GZip compression private void Compress_File(string fileName) { bool useAsync = true; // Specifies asynchronous I/O infile = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.Read, 2000, useAsync); buffer = new byte[infile.Length]; int ln = buffer.Length; // Read file and let callback method handle compression IAsyncResult ar = infile.BeginRead(buffer, 0, ln, new AsyncCallback(Zip_Completed), fileName); // } // Callback method that compresses raw data and stores in file private void Zip_Completed(IAsyncResult asResult) { // Retrieve file name from state object string filename = (string)asResult.AsyncState; infile.EndRead(asResult); // Wrap up asynchronous read infile.Close(); // MemoryStream ms = new MemoryStream(); // Memory stream will hold compressed data GZipStream zipStream = new GZipStream(ms, CompressionMode.Compress, true); // Write raw data in compressed form to memory stream zipStream.Write(buffer, 0, buffer.Length); zipStream.Close(); // Store compressed data in a file FileStream fs = new FileStream(filename+".gz", FileMode.OpenOrCreate,FileAccess.Write,FileShare.Read); byte[] compressedData = ms.ToArray(); fs.Write(compressedData, 0, compressedData.Length); fs.Close(); }
As a rule, asynchronous techniques are not required for file I/O. In fact, for read and write operations of less than 64KB, .NET uses synchronous I/O even if asynchronous is specified. Also, note that if you specify asynchronous operation in the FileStream constructor (by setting the useAsync parameter to true), and then use synchronous methods, performance may slow dramatically. As we demonstrate in later chapters, asynchronous techniques provide a greater performance boost to networking and Web Services applications than to file I/O. |
< Day Day Up > |