[ Team LiB ] Previous Section Next Section

21.3 Asynchronous I/O

All the programs you've looked at so far perform synchronous I/O, meaning that while your program is reading or writing, all other activity is stopped. It can take a long time (relatively speaking) to read data to or from the backing store, especially if the backing store is a slow disk or (horrors!) a slow network.

With large files, or when reading or writing across the network, you'll want asynchronous I/O, which allows you to begin a read and then turn your attention to other matters while the Common Language Runtime (CLR) fulfills your request. The .NET Framework provides asynchronous I/O through the BeginRead( ) and BeginWrite( ) methods of Stream.

The sequence is to call BeginRead( ) on your file and then to go on to other, unrelated work while the read progresses in another thread. When the read completes, you are notified via a callback method. You can then process the data that was read, kick off another read, and then go back to your other work.

In addition to the three parameters you've used in the binary read (the buffer, the offset, and how many bytes to read), BeginRead( ) asks for a delegate and a state object.

The delegate is an optional callback method, which, if provided, is called when the data is read. The state object is also optional. In this example, pass in null for the state object. The state of the object is kept in the member variables of the test class.

You are free to put any object you like in the state parameter, and you can retrieve it when you are called back. Typically (as you might guess from the name), you stash away state values that you'll need on retrieval. The state parameter can be used by the developer to hold the state of the call (paused, pending, running, etc.).

In this example, create the buffer and the Stream object as private member variables of the class:

public class AsynchIOTester
{
    private Stream inputStream;       
    private byte[] buffer;          
    const int BufferSize = 256;

In addition, create your delegate as a private member of the class:

private AsyncCallback myCallBack; // delegated method

The delegate is declared to be of type AsyncCallback, which is what the BeginRead( ) method of Stream expects.

An AsyncCallBack delegate is declared in the System namespace as follows:

public delegate void AsyncCallback (IAsyncResult ar);

Thus this delegate can be associated with any method that returns void and that takes an IAsyncResult interface as a parameter. The CLR will pass in the IAsyncResult interface object at runtime when the method is called. You only have to declare the method:

void OnCompletedRead(IAsyncResult asyncResult)

and then hook up the delegate in the constructor:

AsynchIOTester( )
{
        //...
     myCallBack = new AsyncCallback(this.OnCompletedRead);
}

Here's how it works, step by step. In Main( ), create an instance of the class and tell it to run:

public static void Main( )
{
    AsynchIOTester theApp = new AsynchIOTester( );    
    theApp.Run( );
}

The call to new invokes the constructor. In the constructor, open a file and get a Stream object back. Then allocate space in the buffer and hook up the callback mechanism:

AsynchIOTester( )
{
    inputStream = File.OpenRead(@"C:\test\source\AskTim.txt");
    buffer = new byte[BufferSize];
    myCallBack = new AsyncCallback(this.OnCompletedRead);
}

This example needs a large text file. I've copied a column written by Tim O'Reilly ("Ask Tim") from http://www.oreilly.com into a text file named AskTim.txt. I placed that in a subdirectory I created named test\source on my C: drive. You can use any text file in any subdirectory.

In the Run( ) method, call BeginRead( ), which will cause an asynchronous read of the file:

inputStream.BeginRead(
     buffer,             // where to put the results
     0,                  // offset
     buffer.Length,      // BufferSize
     myCallBack,         // call back delegate
     null);              // local state object

Then go on to do other work. In this case, simulate useful work by counting up to 500,000, displaying your progress every 1,000:

for (long i = 0; i < 500000; i++)        
{
    if (i%1000 == 0)
    {
        Console.WriteLine("i: {0}", i);
    }
}

When the read completes, the CLR will call your callback method:

void OnCompletedRead(IAsyncResult asyncResult)
{

The first thing to do when notified that the read has completed is find out how many bytes were actually read. Do so by calling the EndRead( ) method of the Stream object, passing in the IAsyncResult interface object passed in by the CLR:

int bytesRead = inputStream.EndRead(asyncResult);

EndRead( ) returns the number of bytes read. If the number is greater than zero, you'll convert the buffer into a string and write it to the console, and then call BeginRead( ) again, for another asynchronous read:

if (bytesRead > 0)
{
    String s = 
    Encoding.ASCII.GetString (buffer, 0, bytesRead);
    Console.WriteLine(s);
    inputStream.BeginRead(
    buffer, 0, buffer.Length, 
    myCallBack, null);
}

The effect is that you can do other work while the reads are taking place, but you can handle the read data (in this case, by outputting it to the console) each time a buffer-ful is ready. Example 21-7 provides the complete program.

Example 21-7. Implementing asynchronous I/O
namespace Programming_CSharp
{
   using System;
   using System.IO;
   using System.Threading;
   using System.Text;

   public class AsynchIOTester
   {
      private Stream inputStream;   
    
      // delegated method
      private AsyncCallback myCallBack; 

      // buffer to hold the read data
      private byte[] buffer;         
 
      // the size of the buffer
      const int BufferSize = 256;

      // constructor
      AsynchIOTester( )
      {
         // open the input stream
         inputStream = 
            File.OpenRead(
            @"C:\test\source\AskTim.txt");

         // allocate a buffer
         buffer = new byte[BufferSize];

         // assign the call back
         myCallBack = 
            new AsyncCallback(this.OnCompletedRead);
      }

      public static void Main( )
      {
         // create an instance of AsynchIOTester
         // which invokes the constructor
         AsynchIOTester theApp = 
            new AsynchIOTester( );           

         // call the instance method
         theApp.Run( );
      }

      void Run( )
      {
         inputStream.BeginRead(
            buffer,             // holds the results
            0,                  // offset
            buffer.Length,      // (BufferSize)
            myCallBack,         // call back delegate
            null);              // local state object

         // do some work while data is read
         for (long i = 0; i < 500000; i++)        
         {
            if (i%1000 == 0)
            {
               Console.WriteLine("i: {0}", i);
            }
         }
            
      }

      // call back method
      void OnCompletedRead(IAsyncResult asyncResult)
      {
         int bytesRead = 
            inputStream.EndRead(asyncResult);

         // if we got bytes, make them a string 
         // and display them, then start up again. 
         // Otherwise, we're done.
         if (bytesRead > 0)
         {
            String s = 
               Encoding.ASCII.GetString(buffer, 0, bytesRead);
            Console.WriteLine(s);
            inputStream.BeginRead(
               buffer, 0, buffer.Length, myCallBack, null);
         }
      }
   }
}
Output (excerpt):
i: 47000
i: 48000
i: 49000
Date: January 2001
From: Dave Heisler
To: Ask Tim
Subject: Questions About O'Reilly
Dear Tim,
I've been a programmer for about ten years. I had heard of 
O'Reilly books,then...
Dave,
You might be amazed at how many requests for help with 
school projects I get;  
i: 50000
i: 51000
i: 52000

The output reveals that the program is working on the two threads concurrently. The reads are done in the background while the other thread is counting and printing out every thousand. As the reads complete, they are printed to the console, and then you go back to counting. (I've shortened the listings to illustrate the output.)

In a real-world application, you might process user requests or compute values while the asynchronous I/O is busy retrieving or storing to a file or database.

    [ Team LiB ] Previous Section Next Section