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 which 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’ll 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, you’ll 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 as a parameter an
IAsyncResult
interface. 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( )
,
you 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, you open a file and get a Stream
object back. You then allocate space in the buffer and hook up the
callback mechanism:
AsynchIOTester( ) { inputStream = File.OpenRead(@"C: estsourceAskTim.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 and placed that in a subdirectory I created named testsource on my C: drive. You can use any text file in any subdirectory.
In the Run( )
method, you 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
You then go on to do other work. In this case you’ll 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 you do when notified that the read has completed is
find out how many bytes were actually read. You 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-full 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: estsourceAskTim.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 a database.
3.136.17.12