Click here to Skip to main content
15,881,559 members
Articles / Programming Languages / C#

The .NET Asynchronous I/O Design Pattern

Rate me:
Please Sign up or sign in to vote.
4.50/5 (10 votes)
19 Feb 2010CPOL5 min read 32.8K   37   2
Asynchronous operations allow a program to perform time consuming tasks on a background thread while the main application continues to execute. However, managing multiple threads and cross-thread communication adds complexity to your code. Fortunately, the .NET Framework has a useful Design Pattern

Introduction

I recently blogged on improving system responsiveness by using asynchronous operations[^]. Asynchronous operations allow a program to perform time consuming tasks on a background thread while the main application continues to execute. For example, consider when a program makes a request to a remote system. In a single-threaded scenario, the call is made and the CPU goes idle as the caller waits on the server's processing time and the network latency. If this waiting time can be delegated to a separate thread of execution, the program can complete other tasks until it receives notification that the background work is complete.

However, managing multiple threads and cross-thread communication adds complexity to your code. Fortunately, the .NET Framework has a useful Design Pattern applied to its I/O classes which easily enables asynchronous calls. Let's take a look at an example.

Async I/O for a DNS lookup

Suppose you need to lookup the IP address of a host. The simplest way to do this is to use the System.Net.Dns class:

C#
IPAddress[] hostAddresses = Dns.GetHostAddresses("www.gavaghan.org");

A DNS lookup doesn't take terribly long and, in most cases, the synchronous example above is fine. DNS servers are highly efficient, and local DNS servers will cache authoritative data to optimize response times.

However, suppose you're implementing a high performance mail server that detects spam by querying multiple real time DNS blacklists[^]. For every incoming message, you must execute a dozen DNS operations:

C#
IPAddress[] spamcop = Dns.GetHostAddresses("22.154.199.213.bl.spamcop.net");
IPAddress[] spamhaus = Dns.GetHostAddresses("22.154.199.213.pbl.spamhaus.org");
IPAddress[] fiveten = Dns.GetHostAddresses(
                          "22.154.199.213.blackholes.five-ten-sg.com");
//
// . . . and a dozen others
//

Each synchronous lookup blocks waiting for a response before moving on to the next lookup. The cumulative effect of these delays will get costly. Ideally, you'd want to perform each lookup on its own thread and let the requests run concurrently.

So, let's implement a method that looks like this:

C#
public class AsyncDNSExample
{
    public List<IPAddress> MultiHostLookup(List<string> hosts)
    {

This method will accept a list of host names, execute concurrent DNS lookups for all of them, and return with a list of resolved addresses. Here's how we might use this method:

C#
class Program
{
    static void Main()
    {
      // build list of hosts to lookup
      List<string> hosts = new List<string>();
      hosts.Add("www.gavaghan.org");
      hosts.Add("www.itko.com");
      hosts.Add("sombrita.com");

      // perform the concurrent lookup
      AsyncDNSExample lookup = new AsyncDNSExample();
      List<IPAddress> addressList = lookup.MultiHostLookup(hosts);

      // write out the results
      foreach (IPAddress address in addressList)
      {
        Console.WriteLine(address);
      }
    }
}

Begin and End methods

Many of .NET's I/O classes have asynchronous versions of their synchronous methods. For example, the Read() and Write() methods on System.IO.Stream have respective BeginRead() and BeginWrite() counterparts. System.Net.Sockets.Socket has BeginAccept() and BeginConnect(). And, in benefit of this example, Dns has BeginGetHostAddresses().

All of the Begin* methods cause the object's work to execute on a worker thread in the .NET thread pool. These methods take the same parameters as their synchronous counterparts, plus two additional parameters supporting the async framework.

For example, here's the signature for BeginGetHostAddresses():

C#
public static IAsyncResult BeginGetHostAddresses( string hostNameOrAddress, 
                           AsyncCallback requestCallback, Object state )

One added parameter is an AsyncCallback delegate. The delegate identifies the callback method .NET will invoke once asynchronous processing has completed. The callback method takes a single parameter of type IAsyncResult. The IAsyncResult object must be used to access the result of the asynchronous call.

The second added parameter is an arbitrary state object (possibly null) that may be used to coordinate between the caller and the callback. The state object is made available to the callback method through the IAsyncResult parameter. An example is included a little later.

For each Begin* call, a corresponding End* call must be invoked to get the results of the method. The End* methods block synchronously until processing has been completed. However, when called from within the callback method, End* methods return immediately because, at that point, the work is known to be done.

Let's take a look at how our MultiHostLookup() method can be implemented using the asynchronous version of GetHostAddresses():

C#
public class AsyncDNSExample
{
    public List<IPAddress> MultiHostLookup(List<string> hosts)
    {
      // we'll fill this list with the result of the DNS lookups
      List<IPAddress> addressList = new List<IPAddress>();

      foreach (string host in hosts)
      {
        // begin an asynchronous lookup for each host
        Dns.BeginGetHostAddresses( host, 
            GetHostAddressesCallback, addressList );
      }

      //
      // we can do additional work here while the
      // DNS lookups continue in parallel
      //

      lock (addressList)
      {
        // ensure all lookups have returned, otherwise wait
        while (addressList.Count != hosts.Count)
        {
          Monitor.Wait(addressList);
        }
      }

      return addressList;
    }
}

This method begins by allocating the List<IPAddress> object we'll use to return our resolved addresses. Then, we loop over each of the host names in the hosts List<string> and call BeginGetHostAddresses(). Once this loop completes, all of the DNS queries will execute in parallel.

Notice that the first parameter to BeginGetHostAddresses() is a host name - just like its synchronous counterpart. For the second parameter, we pass a reference to our callback method, GetHostAddressesCallback(), which is defined below. The third parameter is our result List<IPAddress>. This will make the List available to the callback method. When each DNS query completes, the callback method can update the list with each resolved address.

At this point, if we wanted to, we could code other logic to execute as we wait for the queries to complete.

Finally, we check the length of the list of resolved IP addresses to see if it's the same length as the list of host names. To do this, we must first lock the address list object (after all, we don't want our address list modified on a callback thread at the same time we're trying to inspect it). If the list sizes are equal, we know all lookups have completed and we can return from the method. Otherwise, we release the lock on the address list and block until receiving notification from the callback thread.

It's all pretty simple. Now, let's see how to implement the callback method:

C#
public class AsyncDNSExample
{
    private void GetHostAddressesCallback(IAsyncResult result)
    {
      // This method may fail with a SocketException, particularly
      // if the host is not found. A more robust solution would
      // handle such cases.
      IPAddress[] addresses = Dns.EndGetHostAddresses(result);

      // for simplicity, we'll take the first address
      IPAddress address = addresses[0];

      // the address list we passed in is accessbile from AsyncState
      List<IPAddress> addressList = 
          (List<IPAddress>)result.AsyncState;

      // we need to ensure updates to the address list are threadsafe
      lock (addressList)
      {
        addressList.Add(address);

        // notify listeners that another address has been added
        Monitor.PulseAll(addressList);
      }
    }

    // this is our public method for performing multiple, concurrent
    // DNS requests
    public List<IPAddress> MultiHostLookup(List<string> hosts)
    {
      . . . .
    }
}

The first thing that happens is the call to EndGetHostAddresses() with the IAsyncResult object passed in. This call returns immediately with the result of the DNS query (it returns an array of IP addresses but, for simplicity, we'll assume the first one is all we need).

Next, we get a reference to the List<IPAddress> object passed in by the Begin* call. This is where we're going to save our resolved IP addresses. However, for thread safety, we can only add our result from within a lock block. We don't want to be manipulating the list at the same time as another callback!

Finally, we pulse all threads listening on the result object. This schedules the thread executing MultiHostLookup() to check if all of the results have been received.

Conclusion

Using asynchronous I/O can make your applications faster and your user interfaces more responsive - particularly when executing long running tasks and tasks that would otherwise leave the CPU idle. However, even with the .NET design pattern, multithreaded programming always adds to code complexity. So, only leverage this framework where performance optimization is required.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
United States United States
Mike Gavaghan opines on C# and .Net in his blog Talk Nerdy To Me[^]. He is a Microsoft Certified Professional Developer working as a C#/.Net software consultant based in Dallas, Texas.

Since 1992, he has supported clients in diverse businesses including financial services, travel, airlines, and telecom. He has consulted at both mammoth enterprises and small startups (and sees merits and problems in both).

You may also view his profile on LinkedIn[^].

Comments and Discussions

 
Generalsimpler solution Pin
Mr.PoorEnglish20-Mar-10 2:25
Mr.PoorEnglish20-Mar-10 2:25 
GeneralA bigger reason for needing async I/O Pin
supercat924-Feb-10 12:29
supercat924-Feb-10 12:29 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.