13.2. Asynchronous Programming

 < 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:

  • What type of communication between the main thread and worker thread is required? The code on the worker thread can be invoked and forgotten, or it may be necessary for the main thread to know when the task is completed.

  • How does the main thread know when the worker thread is completed? Two approaches are available: the callback technique, in which the worker thread returns control to the main thread when it is finished; or a polling approach, in which the main thread calls a method that returns the results of the worker thread execution.

  • How to synchronize thread requests for the same resources? The issues here are similar to those faced when synchronizing access to a database. The integrity of the data must be maintained and deadlock situations must be avoided.

  • How to shutdown an application while worker threads are still executing? Several choices are available: They can be terminated; the main application can continue to run until all threads finish; or the main application can end and allow the threads to continue running.

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:

  • EndInvoke. After BeginInvoke is called, the main thread can continue working and then call this method. The call to EndInvoke blocks process on the main thread until the asynchronous worker thread completes its execution. This should never be used on a thread that services a user interface because it will lock up the interface.

  • Use a WaitHandle Synchronization object. The IAsyncResult object returned by BeginInvoke has a WaitHandle property that contains a synchronization object. The calling thread can use this object (or objects) to wait until one or more asynchronous tasks complete execution.

  • CallBack Method. As mentioned earlier, one of the parameters to BeginInvoke can be a delegate that specifies a method to be called when the asynchronous method finishes. Because the callback method is run on a new thread from the thread pool, this technique is useful only when the original calling thread does not need to process the results of the asynchronous method.

  • Polling. The IAsyncResult object has an IsCompleted property that is set to true when the method called by BeginInvoke finishes executing. Polling is achieved by periodically checking this value.

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.

Table 13-1. Selected IAsyncResult Properties

Property

Description

AsyncState

The object that is passed as the last parameter to the BeginInvoke method.

AsyncWaitHandle

Returns a WaitHandle type object that is used to wait for access to resources. Access is indicated by a "signal" that the asynchronous task has completed. Its methods allow for various synchronization schemes based on one or multiple active threads:

WaitOne. Blocks thread until WaitHandle receives signal.

WaitAny. Waits for any thread to send a signal (static).

WaitAll. Waits for all threads to send a signal (static).

AsyncDelegate

Returns the delegate used for the asynchronous call.

IsCompleted

Boolean value that returns the status of the asynchronous call.


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

Applications that need to host ActiveX controls or interact with the clipboard must apply the STAThread (single-threaded apartment) attribute to their Main() method. Unfortunately, you cannot use WaitAll() in applications that have this attribute due to conflicts between COM and the Win32 method that WaitAll wraps. Visual Studio users should be aware of this because C# under VS.NET adds the attribute by default.


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:

  • The BeginInvoke signature includes optional data parameters as well as a delegate containing the callback method and a state object:

     bd.BeginInvoke(ht,wt,new AsyncCallback(OnCallBack),name); 

  • The final parameter can be information of any type that is useful to the code that receives control after the asynchronous method completes. In this example, we pass the name of the person whose BMI is calculated.

  • The callback method must have the signature defined by the AsyncCallback delegate.

     public delegate void AsyncCallback(IAsyncResult    asyncResult); 

  • The callback method must cast its parameter to an AsyncResult type in order to access the original delegate and call EndInvoke.

     AsyncResult asyncObj = (AsyncResult)asResult; // Get the original delegate bmiDelegate bd= (bmiDelegate)asyncObj.AsyncDelegate; decimal bmi = bd.EndInvoke(asResult); 

  • The call to EndInvoke should always be inside an exception handling block. When an exception occurs on an asynchronous method, .NET catches the exception and later rethrows it when EndInvoke is called.

  • The BMICaller method is invoked from an instance of BMIExample using the following code. Note that the main thread is put to sleep so it does not end before the result is calculated.

     BMIExample bmi = new BMIExample(); bmi.BMICaller(68,122, "Diana"); Thread.Sleep(500);  // Give it time to complete 

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 > 


    Core C# and  .NET
    Core C# and .NET
    ISBN: 131472275
    EAN: N/A
    Year: 2005
    Pages: 219

    flylib.com © 2008-2017.
    If you may any questions please contact us: flylib@qtcs.net