Asynchronous Programming
.NET supports a design pattern for asynchronous programming. This pattern is present in many places in .NET (including I/O operations, as noted earlier, and as we will see in Chapter 11). Asynchronous programming provides a way for you to provide a method call without blocking the method caller. From the perspective of the client, the asynchronous model is easier to use than threading. It offers much less control over the synchronization than using synchronization objects, however, and the class designer would probably find threading to be much easier to use.
The Asynchronous Design Pattern
This design pattern is composed of two parts: a set of methods and an interface IAsyncResult. The methods of the pattern are
IAsyncResult *BeginXXX( [InputParams], AsyncCallback *cb, Object *AsyncObject) [ReturnValue] EndXXX([OutputParams], IAsyncResult *ar);
As a design pattern, the XXX represents the actual method being called asynchronously (e.g., BeginRead/EndRead for the System::IO::FileStream class). The BeginXXX should pass all input parameters of the synchronous version (in, in/out, and ref) as well as the AsyncCallback and AsyncObject parameters. The EndXXX should have all the output parameters of the synchronous version (ref, out, and in/out) parameters in its signature. It should return whatever object or value that the synchronous version of the method would return. It should also have an IAsyncResult parameter. A CancelXXX can also be provided if it makes sense.
The AsyncCallback is a delegate that represents a callback function.
public __delegate void AsyncCallback(IAsyncResult *ar);
The AsyncObject is available from IAsyncResult. It is provided so that in the callback function you can distinguish which asynchronous read generated the callback.
The framework uses this pattern so that the FileStream synchronous Read method can be used asynchronously. Here is the synchronous FileStream::Read method:
int Read( __in unsigned char* array __gc[], int offset, int count);
Here is the asynchronous version using the design pattern:
IAsyncResult *BeginRead( __in unsigned char* array __gc[], int offset, int numBytes, AsyncCallback *userCallback, Object *stateObject); int EndRead(IAsyncResult *asyncResult);
Any exception thrown from BeginXXX should be thrown before the asynchronous operation starts. Any exceptions from the asynchronous operation should be thrown from the EndXXX method.
IAsyncResult
IAsyncResult is returned by a BeginXXX method (such as BeginRead). This interface has four elements:
public __gc __interface IAsyncResult { public: bool get_IsCompleted(); bool get_CompletedSynchronously(); WaitHandle* get_AsyncWaitHandle(); Object* get_AsyncState(); }
The get_IsCompleted field is set to true after the call has been processed by the server. The client can destroy all resources after get_IsCompleted is set to true. If BeginXXX completed synchronously, get_CompletedSynchronously is set to true. Most of the time this will be ignored and set to the default value of false. In general, a client never knows whether the BeginXXX method executed asynchronously or asynchronously. If the asynchronous operation is not finished, the EndXXX method will block until the operation is finished.
The get_AsyncWaitHandle returns a WaitHandle that can be used for synchronization. As we discussed previously, this handle can be signaled so that the client can wait on it. Since you can specify a wait time period, you do not have to block forever if the operation is not yet complete.
The get_AsyncState is the object provided as the last argument in the BeginXXX call. It allows you to differentiate asynchronous reads in the callback.
Using Delegates for Asynchronous Programming
Any developer of .NET objects who wants to provide an asynchronous interface should follow the pattern just described. Nonetheless, there is no need for most developers to develop a custom asynchronous solution for their objects. Delegates provide a very easy way to support asynchronous operations on any method without any action on the class developer's part. Of course, this has to be done with care because the object may have been written with certain assumptions about which thread it is running on and its synchronization requirements.
The two Asynch examples16 use the Customers object from our case study Customer assembly. The first example, AsynchWithoutCallback, registers new customers asynchronously and does some processing while waiting for each registration to finish. The second example, AsynchWith Callback, uses a callback function with the asynchronous processing. In addition to allowing the program to do processing while waiting for the registrations to finish, the callback allows the system to take some asynchronous action for each individual registration.
In the examples, we just print out to the console to show where work could be done. To increase the waiting time to simulate longer processing times, we have put calls to Thread::Sleep() in Customers::RegisterCustomer as well as in the sample programs. Now let us look at the code within the examples.
Suppose the client wants to call the RegisterCustomer method asynchronously. The caller simply declares a delegate with the same signature as the method.
public __delegate int RegisterCustomerCbk( String *FirstName, String *LastName, String *EmailAddress);
You then make the actual method the callback function:
RegisterCustomerCbk *rcc = new RegisterCustomerCbk( customers, Customers::RegisterCustomer);
Begin/End Invoke
When you declare a delegate, the compiler generates a class with three methods: BeginInvoke, EndInvoke and Invoke. The BeginInvoke and EndInvoke are type-safe methods that correspond to the BeginXXX and EndXXX methods and allow you to call the delegate asynchronously. The Invoke method is what the compiler uses when you call a delegate.17 To call RegisterCustomer asynchronously, just use the BeginInvoke and EndInvoke methods.
RegisterCustomerCbk *rcc = new RegisterCustomerCbk( customers, Customers::RegisterCustomer); for(int i = 1; i < 5; i++) { firstName = String::Concat( "FirstName", i.ToString()); lastName = String::Concat( "SecondName", (i * 2).ToString()); emailAddress = String::Concat( i.ToString(), ".biz"); IAsyncResult *ar = rcc->BeginInvoke( firstName, lastName, emailAddress, 0, 0); while(!ar->IsCompleted) { Console::WriteLine( "Could do some work here while waiting for customer @@ registration to complete."); ar->AsyncWaitHandle->WaitOne(1, false); } customerId = rcc->EndInvoke(ar); Console::WriteLine( " Added CustomerId: {0}", customerId.ToString()); }
The program waits on the AsyncWaitHandle periodically to see if the registration has finished. If it has not, some work could be done in the interim. If EndInvoke is called before RegisterCustomer is complete, EndInvoke will block until RegisterCustomer is finished.
Asynchronous Callback
Instead of waiting on a handle, you could pass a callback function to BeginInvoke (or a BeginXXX method). This is done in the AsynchWithCallback example.
RegisterCustomerCbk *rcc = new RegisterCustomerCbk( customers, Customers::RegisterCustomer); AsyncCallback *cb = new AsyncCallback(this, CustomerCallback); Object *objectState; IAsyncResult *ar; for(int i = 5; i < 10; i++) { firstName = String::Concat( "FirstName", i.ToString()); lastName = String::Concat( "SecondName", (i * 2).ToString()); emailAddress = String::Concat(i.ToString(), ".biz"); objectState = __box(i); ar = rcc->BeginInvoke( firstName, lastName, emailAddress, cb, objectState); } Console::WriteLine( "Finished registrations...could do some work here."); Thread::Sleep(25); Console::WriteLine( "Finished work..waiting to let registrations complete."); Thread::Sleep(1000);
You then get the results in the callback function:
void CustomerCallback(IAsyncResult *ar) { int customerId; AsyncResult *asyncResult = dynamic_cast<AsyncResult *>(ar); RegisterCustomerCbk *rcc = dynamic_cast<RegisterCustomerCbk *> (asyncResult->AsyncDelegate); customerId = rcc->EndInvoke(ar); Console::WriteLine( " AsyncState: {0} CustomerId {1} added.", ar->AsyncState, customerId.ToString()); Console::WriteLine( " Could do processing here."); return; }
You could do some work when each customer registration was finished.
Threading with Parameters
The asynchronous callback runs on a different thread from the one on which BeginInvoke was called. If your threading needs are simple and you want to pass parameters to your thread functions, you can use asynchronous delegates to do this. You do not need any reference to the Threading namespace. The reference to that namespace in the AsynchThreading example is just for the Thread::Sleep method needed for demonstration purposes.
PrintNumbers sums the numbers from the starting integer passed to it as an argument to 10 greater than the starting integer. It returns that sum to the caller. PrintNumbers can be used for the delegate defined by Print.
//AsynchThreading.h using namespace System; using namespace System::Threading; public __delegate int Print(int i); public __gc class Numbers { public: int PrintNumbers(int start) { int threadId = Thread::CurrentThread->GetHashCode(); Console::WriteLine( "PrintNumbers Id: {0}", threadId.ToString()); int sum = 0; for (int i = start; i < start + 10; i++) { Console::WriteLine(i.ToString()); Thread::Sleep(500); sum += i; } return sum; }
The Main routine then defines two callbacks and invokes them explicitly with different starting integers. It waits until both of the synchronization handles are signaled. EndInvoke is called on both, and the results are written to the console.
Numbers *n = new Numbers; Print *pfn1 = new Print(n, Numbers::PrintNumbers); Print *pfn2 = new Print(n, Numbers::PrintNumbers); IAsyncResult *ar1 = pfn1->BeginInvoke(0, 0, 0); IAsyncResult *ar2 = pfn2->BeginInvoke(100, 0, 0); WaitHandle *wh [] = new WaitHandle*[2]; wh[0] = ar1->AsyncWaitHandle; wh[1] = ar2->AsyncWaitHandle; // make sure everything is done before ending WaitHandle::WaitAll(wh); int sum1 = pfn1->EndInvoke(ar1); int sum2 = pfn2->EndInvoke(ar2); Console::WriteLine( "Sum1 = {0} Sum2 = {1}", sum1.ToString(), sum2.ToString());
The program's output:
MainThread Id: 2 PrintNumbers Id: 14 0 1 2 3 4 5 6 7 8 9 PrintNumbers Id: 14 100 101 102 103 104 105 106 107 108 109 Sum1 = 45 Sum2 = 1045