Introduction
Executing asynchronous operations is the key to building high-performance and scalable applications. It allows us to execute many operations on a very small number of lines. The ms clr team has designed a pattern that allows developers to easily exploit, called asynchronous programming patterns (APM ).
In FCL, many types support APM, as shown in the following examples:
- The beginread and beginwrite methods are provided for all classes that are derived from system. Io. stream and communicate with the hardware design.
- The system. net. DNS class provides the begingethostaddresses, begingethostbyname, begingethostentry, and beginresolve methods.
- All classes derived from system. net. webrequest provide the begingetrequeststream and begingetresponse methods.
In addition, all Delegate types define a begininvoke method to use APM.
Use APM to perform asynchronous operations restricted by I/O
If you want to use APM to asynchronously read some bytes from a file stream. First, you need to call the constructor of the system. Io. filestream object and accept a system. Io. fileoptions parameter to construct a system. Io. filestream object. For the system. Io. fileoptions parameter, we pass a fileoptions. asynchronous mark, which tells the filestream object that we are going to perform asynchronous read/write operations on the file.
To synchronously read bytes from a filestream object, we can call its read method. The prototype of this method is as follows:
public Int32 Read(Byte[] array, Int32 offset, Int32 count);
The problem with the synchronization method is that the method will not return until all the read bytes have been stored in the byte array, and the efficiency is very low.
To asynchronously read bytes from a file, you can call the beginread method of filestream:
IAsyncResult BeginRead(Byte[] array, Int32 offset, Int32 numBytes, AsyncCallback userCallback, Object stateObject);
Note that the first three parameters of the beginread method are the same as those of the read method. The beginread method actually adds the operation request to the queue of the window Device Driver, while the Windows Device driver knows how to communicate with the correct hardware device. In this way, the hardware takes over the operation and does not need any threads to execute any operation or even wait for the output result. This method is quite efficient.
Other types of I/O operations are similar to file I/O operations. For example, when networkstream is used, I/O requests are added to the queue of the Windows network device driver, and then wait for the response.
The beginread method returns a reference to an object whose type implements the system. iasyncresult interface. When the beginread method is called, it constructs an object to uniquely identify the I/O operation request, adds the request to the queue of the Windows Device Driver, and then returns the iasyncreault object to us. We can regard the iasyncresult object as a receipt. When the beginread method returns, the I/O operation is queued and has not been completed yet. Therefore, we cannot operate on the bytes in the byte array, because the array does not contain the requested data.
In fact, the array may already contain the requested data because I/O operations have been executed asynchronously. However, the data may be read from the server several seconds later. It is also possible that the server network is paralyzed and data will never be read. We need a method to find out the actual situation, and also know when the result is detected. This is called the aggregation of asynchronous operations. APM provides Clustering Techniques for three asynchronous operation results.
When creating a filestream object, you can use the fileoptions. asynchronous mark to specify whether you want to use synchronous or asynchronous operation communication (the same as calling the createfile function of Win32 and passing it a file_flag_overlapped mark ). If this flag is not specified, Windows will perform all operations synchronously on the file. Of course, we can still call the beginread method of filestream. For applications, it seems that the operation is executed asynchronously. However, in fact, the filestream class calls another thread internally to simulate asynchronous behavior. This extra thread is economic and also affects performance.
On the other hand, you can create a filestream object by specifying the fileoptions. asynchronous mark, and then you can call the filestream read method to perform synchronization operations. The file-stream class starts an Asynchronous Operation internally, and then immediately sleeps the calling thread until the operation is completed to simulate synchronization. This method is very inefficient, but it is more efficient than not specifying the fileoptions. asynchronous mark to create a filestream object and then calling the beginread method.
All in all, when using a filestream object, you must first determine whether to perform synchronous I/O operations or asynchronous I/O operations on the file, and specify fileoptions. asynchronous flag (or not specified) indicates our choice. If this flag is specified, you should call the beginread method. If this flag is not specified, the read method is usually called. This will give the program the best performance.
Three Clustering Techniques of APM
APM supports three Clustering Techniques: Wait-until-done, polling, and method callback ).
Skills for waiting for APM to complete Aggregation
To start an asynchronous operation, we can call some beginxxx methods. All these methods queue request operations and return an iasyncresult object to identify the pending operations. To obtain the operation result, we can use the iasyncresult object as the parameter to call the corresponding endxxx method. All endxxx Methods accept an iasyncresult object as its parameter. Basically, when calling the endxxx method, we request the CLR to return the result of an asynchronous operation identified by the iasyncresult object.
If the asynchronous operation has been completed, it will return the result immediately when calling the endxxx method. If the asynchronous operation is not completed, the endxxx method suspends the calling thread until the asynchronous operation is completed, and then returns the result.
The following is a prototype of the filestream endxxx method:
Int32 EndRead(IAsyncResult asyncResult);
The Return Value of the method indicates the number of bytes read from the filestream object.
A complete example of filestream is as follows:
Using system;
Using system. IO;
Using system. Threading;
Public static class Program
{
Public static void main ()
{
// Open the file indicating asynchronous I/O operations
Filestream FS = new filestram (@ "C: \ Boot. ini", filemode. Open, fileaccess. Read, fileshare. Read, 1024, fileoptions. asynchronous );
Byte [] DATA = new byte [100];
// Initialize an asynchronous read operation for the filestream object
Iasyncresult AR = FS. beginread (data, 0, Data. length, null, null );
// Execute some code here
// Suspend the thread until the asynchronous operation ends and obtain the result
Int32 bytesread = FS. endread (AR );
// No operation is performed to close the file
FS. Close ();
// Access the byte array and display the result
Console. writeline ("number of bytes READ = {0}", bytesread );
Console. writeline (bitconverter. tostring (data, 0, bytesread ));
}
}
If you put some code between beginread and endread, you will see some value of APM. The following code substantially modifies the previous program. The new version of the program reads data from multiple streams at the same time.
Private sealed class asyncstreamread
{
Private stream m_stream;
Private iasyncresult m_ar;
Private byte [] m_data;
Public asyncstreamread (Stream stream, int32 numbytes)
{
M_stream = stream;
M_data = new byte [numbytes];
// Initialize an asynchronous read operation for the stream object
M_ar = stream. beginread (m_data, 0, numbytes, null, null );
}
Public byte [] endread ()
{
// Suspend the thread until the asynchronous operation ends and obtain the result
Int32 numbytesread = m_stream.endread (m_ar );
// No operation is performed to close the stream
M_stream.close ();
// Adjust the array size to save space
Array. Resize (ref m_data, numbytesread );
// Return bytes
Return m_data;
}
}
Private Static void readmultiplefiles (Params string [] pathnames)
{
Asyncstreamread [] ASRs = new asyncstreamread [pathnames. Length];
For (int32 n = 0; n <pathnames. length; n ++)
{
// Open the file indicating asynchronous I/O operations
Stream stream = new filestream (pathnames [N], filemode. Open, fileaccess. Read, fileshare. Read, 1024, fileoptions. asynchronous );
// Initialize an asynchronous read operation for the stream object
ASRs [N] = new asyncstreamread (streams, 100 );
}
// All streams have been opened, and all read requests have been queued. They are all concurrently executed.
// Obtain and display the result below
For (int32 n = 0; n <ASRs. length; n ++)
{
Byte [] bytesread = ASRs [N]. endread ();
// Now you can access the byte array and display the result.
Console. writeline ("number of bytes READ = {0}", bytesread. Length );
Console. writeline (bitconverter. tostring (bytesread ));
}
}
Poll clustering skills of APM
The iasyncresult interface defines several read-only attributes:
public interface IAsyncResult
{
Object AsyncState {get;}
WaitHandle AsyncWaitHandle {get;}
Boolean IsCompleted {get;}
Boolean CompletedSynchronously {get;}
}
The efficiency of using the round robin clustering technique is not high. When writing code that uses the polling clustering technique, a thread should regularly ask the CLR to check whether the asynchronous request has been executed. Therefore, it is essentially a waste of time on a thread. Example:
Public static void pollingwithiscompleted ()
{
// Open the file indicating asynchronous I/O operations
Filestream FS = new filestream (@ "C: \ Boot. ini", filemode. Open, fileaccess. Read, fileshare. Read, 1024, fileoptions. asynchronous );
Byte [] DATA = new byte [100];
// Initialize an asynchronous read operation for the filestream object
Iasyncresult AR = FS. beginread (data, 0, Data. length, null, null );
While (! Ar. iscompleted)
{
Console. writeline ("operation not completed; still waiting .");
Thread. Sleep (10 );
}
// Obtain the result. Note: The endread method cannot suspend this thread.
Int32 bytesread = FS. endread (AR );
// Access the array and display the result
Console. writeline ("number of bytes READ = {0}", bytesread );
Console. writeline (bitconverter. tostring (data, 0, bytesread ));
}
The following is an example of another round robin clustering technique. This example queries the asyncwaithandle attribute of the iasyncresult interface:
Public static void pollingwithasyncwaithandle ()
{
// Open the file indicating asynchronous I/O operations
Filestream FS = new filestream (@ "C: \ Boot. ini", filemode. Open, fileaccess. Read, fileshare. Read, 1024, fileoptions. asynchronous );
Byte [] DATA = new byte [100];
// Initialize an asynchronous read operation for the filestream object
Iasyncresult AR = FS. beginread (data, 0, Data. length, null, null );
While (! Ar. asyncwaithandle. waitone (10, false ))
{
Console. writeline ("operation not completed; still waiting .");
}
// Obtain the result. Note: The endread method cannot suspend this thread.
Int32 bytesread = FS. endread (AR );
// Close the file
FS. Close ();
// Access the array and display the result
Console. writeline ("number of bytes READ = {0}", bytesread );
Console. writeline (bitconverter. tostring (data, 0, bytesread ));
}
The asyncwaithandle attribute of the iasyncresult interface returns a reference to an object derived from waithandle, which is usually system. Threading. manualresetevent.
Method callback clustering of APM
Among all APM clustering techniques, the method callback clustering technique is best used. The reason is that this technique never puts a thread in the waiting state, and it does not regularly check whether asynchronous operations are completed, wasting CPU time.
The following is the basic working principle of the method callback clustering technique: first, the asynchronous I/O request is queued and then the thread continues to execute anything it wants to execute. Then, when the I/O request is complete, Windows adds the work item to the queue of the CLR thread pool. Finally, the thread in the thread pool extracts work items from the queue and calls some methods we have written. Now, inside the callback method, we first call the endxxx method to obtain the result of the asynchronous operation, and then we can freely continue to process the result. When the callback method is returned, the thread in the thread pool returns to the thread pool to prepare the service for the next queued work item (or wait for the next work item to appear ).
The last two parameters of each beginxxx method are the same. One is system. asynccallback, which is a delegate type and is defined as follows:
delegate void AsyncCallback(IAsyncResult ar);
This delegate indicates the signature required for the required callback method. For the stateobject parameter of the beginxxx method, any parameters we want to pass can be passed. This parameter only provides a way to transfer some data in the queuing Method to the callback method after the processing is completed. The callback method will receive a reference to an iasyncresult object, and the callback method can query the asyncstate attribute of iasyncresult to obtain the reference of the State object. The sample code is as follows:
Using system;
Using system. IO;
Using system. Threading;
Public static class Program
{
Private Static byte [] s_data = new byte [1, 100];
Public static void main ()
{
// Display the ID of the thread that is executing the main method
Console. writeline ("main thread id = {0}", thread. currentthread. managedthreadid );
// Open the file indicating asynchronous I/O operations
Filestream FS = new filestream (@ "C: \ Boot. ini", filemode. Open, fileaccess. Read, fileshare. Read, 1024, fileoptions. asynchronous );
// Initialize an asynchronous read operation for the filestream object and pass the filestream object FS to the callback method readisdone
FS. beginread (s_data, 0, s_data.length, readisdone, FS );
// Execute some other code here
// Suspends the main thread
Console. Readline ();
}
Private Static void readisdone (iasyncresult AR)
{
// Display the ID of the thread that is executing the readisdone Method
Console. writeline ("readisdone thread id = {0}", thread. currentthread. managedthreadid );
// Extract the filestream object (state) from the iasyncresult object)
Filestream FS = (filestream) Ar. asyncstate;
// Obtain the result
Int32 bytesread = FS. endread (AR );
// Close the file
FS. Close ();
// Access the array and display the result
Console. writeline ("number of bytes READ = {0}", bytesread );
Console. writeline (bitconverter. tostring (data, 0, bytesread ));
}
}