Typically I/O operations are characterized by slow, unpredictable. When an application performs a synchronous I/O operation, it basically discards control of the device that is completing the actual work. For example, if the application calls the Streamread method to read some bytes from FileStream or networkstream, we cannot predict how long the method will take to return. If the file being read is on a local hard disk, the read operation may return immediately. If the remote server that stores the file is offline, the Read method may wait a few minutes and then timeout and throw an exception. During this time, the thread that issued the synchronization request is occupied. If the thread is a UI thread, the application is frozen and stops responding to user input.
A thread that is waiting for synchronous I/O completion is blocked, meaning that the thread is idle, but cannot perform useful work. To improve scalability, many application developers create more threads. Unfortunately, each thread brings considerable administrative overhead, such as its kernel object, user mode and kernel-mode stack, increased environment switching, and DllMain methods with thread attach/detach notifications. The end result is a reduction in scalability.
If your application wants to maintain responsiveness to users, increase scalability and throughput, and improve reliability, I/O operations should not be synchronized. The application should use the common language runtime (CLR) asynchronous Programming Model (APM) to perform asynchronous I/O operations. There are a lot of written information about how to use CLR APM, including my book The 23rd chapter of the second edition of the CLR via C # (Microsoft press®, 2006). But I didn't notice any data explaining how to define a class that provided a way to implement APM. So I decided to focus on this topic in this column.
There are basically four reasons developers want to implement APM. First, you might want to build a class that communicates directly with hardware, such as the file system, network, serial port, or parallel port on your hard disk. As mentioned above, device I/O is unpredictable, so applications should perform asynchronous I/O operations when communicating with the hardware to keep the application responsive, scalable, and reliable.
Fortunately, the Microsoft®.net Framework already contains classes that communicate with many hardware devices. Therefore, you do not need to implement APM yourself unless you are defining a class that communicates with a hardware device (such as a parallel port) that is not supported by the Framework class library (FCL). However, although FCL supports certain devices, it does not support some of these specific features. In this case, you may need to implement APM if you want to perform I/O operations. For example, although FCL provides FileStream classes that allow applications to communicate with disks, FileStream does not allow you to access opportunistic locks (microsoft.com/msj/0100/win32/win320100.aspx), sparse file streams ( microsoft.com/msj/1198/ntfs/ntfs.aspx) or other novel features provided by the NTFS file system. If you are writing a P/invoke wrapper to invoke the WIN32®API that provide these features, you may want the wrapper to support APM so that you can perform the operation asynchronously.
Second, you might want to build an abstraction layer on a class that has already been defined to communicate directly with the hardware. Several examples have been provided in the. NET Framework. For example, suppose you want to send data to a Web service method. On the WEB service client proxy class, there is a way to accept your arguments, which may be a complex set of data structures. Internally, the method serializes these complex data structures into a byte array. It then uses the NetworkStream class, which already has the ability to communicate with the hardware using asynchronous I/O, to send the byte array over the network. Another example appears when you access a database. The Ado.net SqlCommand type provides beginexecutenonquery, Beginexecutereader, and other BeginXxx methods that can parse parameters to send data over the network to the database. When I/O is complete, the corresponding endexecutenonquery, Endexecutereader, and other endxxx methods are invoked. Internally, these endxxx methods analyze the resulting data and return the rich data object to the caller.
Third, your class might provide a method that may take a long time to execute. In this case, you may want to provide the BeginXxx and EndXxx methods to provide convenient APM to the caller. The previous example is ultimately an I/o-intensive operation, and your approach this time is to perform compute-intensive operations. Because it is a computationally intensive operation, you must use a thread to perform the work, and the BeginXxx and EndXxx methods are defined only for the convenience of your class users.