[ Team LiB ] |
21.3 Asynchronous I/OAll 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); }
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/Onamespace 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 ] |