Asynchronous Method Invocation in COM+

. Fritz Onion works at DevelopMentor, developing and teaching courses in MFC, ATL, and COM. He can be contacted at [email protected].


WITH ANY LUCK, by the time this article is published, the long-awaited Windows2000 will have been released. Because this should be my first post-Windows2000 column, I will discuss one of the newer features rolled out with COM+—the ability to issue nonblocking method calls.

Accompanying this article is an online set of samples implementing each concept covered. You can find these samples at www.develop.com/hp/onion/cpprep/async.zip.

ASYNCHRONY Method calls made to COM objects are blocking calls. This means that a client thread making a call to a COM object will not continue execution until that call is completed. This is true for COM objects that live in the same process, on different processes, or even on different machines. When a client makes a call to an object that lives on the same thread as the client, the object's method is run on the same thread, and thus is a synchronous call. When a client makes a call to an object that lives in a separate process (perhaps on a different machine), however, the client thread will block while an RPC request is sent on its behalf to invoke the function remotely. Once the RPC response is received, the client thread will be awakened and handed the results. This blocking method invocation in COM is one of the foundations of location transparency—client code is the same regardless of where an object resides. Synchronous method invocation greatly simplifies our lives as distributed application developers, essentially making an entire network of computers behave as if all code was executing on a single thread.

There are times, however, when you don't want all of your method calls to be synchronous. Perhaps you are making a call to an object across a network that you know may take some time, but you don't want your user interface to freeze while you wait for the call to finish. Or perhaps you are in the middle of performing an intensive computation when you make a method call to an object, and you want to be able to continue with your computation while you wait for that method call to complete. Both of these are valid reasons to want to issue calls to an object as nonblocking, asynchronous calls.

The version of COM that ships with Windows NT 4.0 has no explicit support for nonblocking method invocation. One of the most common ways to achieve asynchronous calling under Windows NT 4.0 is to spawn a thread to issue the call on our behalf. As an example of this, consider creating a COM object, Foo, and trying to invoke a method on its IFoo interface asynchronously (error-checking omitted):

Listing 1.

By spawning a thread to invoke the method call, we were able to spin a do loop until the gDoneEvent was signaled, indicating call completion. Had it been a thread with windows, we could have run a message loop to make sure our user interface stayed alive.

Asynchronous Interfaces There are two problems with this technique. First, it is a bad idea to spawn threads in such a way that you can't control how many threads you are going to create. If we attempt to make a large number of asynchronous method calls simultaneously, we are going to spawn a large number of threads, which can quickly swamp a program. The second problem is that we are spawning a thread that is really not going to do anything. Remember, when a method call is made to an object that lives in another process, the calling thread is blocked until the response packet is received. Thus, we are creating a thread that is going to do nothing but block and then trigger an event. If we had a way of telling COM to not block our thread when we make the call, we wouldn't need to spawn a second thread.

This is exactly what COM+ provides through its nonblocking method invocation support. A client can request that a call be made in a nonblocking fashion so that it can continue with its business while it waits for the call to finish. To do this, COM+ needs to provide three fundamental pieces of functionality:
  1. A way to indicate that a call is being made asynchronously;

  2. A polling or callback mechanism to know when the call is complete; and

  3. A way to receive the results of the call once it is complete.
Two of these pieces of functionality come in the form of an asynchronous interface, which is where we will start.

For an object to support asynchronous method invocation, it must support an interface that has the async_uuid attribute associated with it:

Listing 2.

When an interface is marked with the async_uuid attribute, the MIDL compiler will generate two versions of the interface—a synchronous one and an asynchronous one. The synchronous version of the normal interface that would be generated by the MIDL compiler for IFoo is

Listing 3.

The asynchronous version of the interface will look similar, except that for every method defined in the synchronous interface, there will be two methods in the asynchronous one. These two methods will be called Begin_xxx and Finish_xxx, where xxx is the synchronous method name. The begin method will contain all of the in parameters defined in the synchronous method, and the end method will contain all of the out parameters. This interface will be named AsyncIxxx and will have a distinct IID from the synchronous version. Thus, the asynchronous version of our IFoo interface looks like

Listing 4.

Once a client has a pointer to this asynchronous version of our interface, he will be able to invoke a nonblocking call by calling the Begin_DoFoo method, and he will be able to collect the results of the call by calling Finish_DoFoo. The only piece of functionality missing is a mechanism for determining when the method call is complete.

Call Objects I have been intentionally vague about how a client invokes a call asynchronously because there is one other entity that must come into the picture before it can—the call object. To invoke a nonblocking call on an object, a client must first obtain a pointer to a call object. Call objects are created using the CreateCall method of the ICallFactory interface:

Listing 5.

The ICallFactory will be implemented by any object that supports nonblocking invocation, either directly by the object, or implicitly through the proxy manager. The call object obtained through CreateCall() will implement the asynchronous interface requested as the first parameter, as well as ISynchronize. It is in the ISynchronize interface that we find the third of the three pieces of functionality necessary to make nonblocking calls. A client can use the Wait() method of ISynchronize to poll for method completion:

Listing 6.

The complete process of invoking a call asynchronously involves obtaining a pointer to ICallFactory; requesting a new call object; calling the Begin_xxx method on the asynchronous interface of the call object; polling for completion using ISynchronize; and finally, calling the Finish_xxx method on the asynchronous interface to collect the results of the call. Here is a program that invokes a call to DoFoo() asynchronously using COM+ nonblocking invocation instead of spawning a second thread (error-checking omitted):

Listing 7.

Notice that this code looks very similar to the code shown earlier that spawned a second thread to achieve the same effect. In this version, however, the need for a second thread was obviated by the nonblocking method call.

In this example, we polled to find out when the call was complete by calling the Wait() method of the ISynchronize interface defined by the call object. It is also possible to receive a callback by providing your own implementation of ISynchronize and passing it in as the second parameter to CreateCall(). Once you have created the call object, you blind-aggregate it with your ISynchronize implementation, and the ISynchronize::Signal() method will be called on your object when the method completes. The online sample contains an example of both a polling client and a callback client.

Asynchronous Servers The good news about building an object that supports asynchronous method invocation is that there is usually nothing extra to do. All of the details of managing asynchronous method calls are taken care of for you by the proxy/stub layer that comes between you and your clients. You just add the async_uuid attribute to each interface that you want to be invoked asynchronously, and then build your objects as usual.

The key to nonblocking method invocation working without any real code changes on the server side is in the proxy/stub for the asynchronous interface. When a client connects to an object that is in a different location (apartment, process, etc.), COM will construct a proxy through which the client will communicate to the object, and a stub on the server side from which the object will receive method calls. This proxy/stub architecture is what enables location transparency in COM, by implicitly turning what look like normal method calls into RPC requests, for example. The proxy manager, which is responsible for creating proxies on the client's behalf, implicitly implements ICallFactory on behalf of any interface that was labeled with async_uuid. Its implementation of ICallFactory::CreateCall will create a generic implementation of the asynchronous version of your interface (AsyncIFoo in our case) that will manage any nonblocking calls made by the client to the object. This generic call object will take care of dispatching calls to the object's stub, waiting for the method to return, and signaling a system-provided implementation of ISynchronize when the results are returned. When the client calls the Begin_XXX method on an interface, it will return immediately to the client instead of blocking like proxies normally would. To the server, there is no difference between method calls made asynchronously and those made synchronously. Both types of calls come through the standard stub and are dispatched to exactly the same code.

Servers don't always want to let their proxies manage asynchronous calls on their behalf. To override the default handling of asynchronous calls, a server object may implement ICallFactory itself and provide its own implementation of the asynchronous version of its interface. Consider this scenario: Your server implements several COM classes and may have many clients attached to many instances of these classes at any given time. Now suppose that many of these objects access a local piece of hardware that is capable of servicing only one request at a time. If we use the default behavior of call dispatching to an object in our server, we will potentially tie up many RPC threads waiting for access to our one piece of hardware. As more and more client method invocations come into our server, more and more RPC threads will be created to service the requests. Each of these newly created threads will sit idly blocking while awaiting access to our serial hardware.

By implementing ICallFactory to return a custom implementation of our asynchronous interface (AsyncIFoo in our case) we could return from the Begin_DoFoo method invocation immediately, thus releasing the RPC thread—after placing a request (along with some call state information and the implementation of our asynchronous interface) in a queue to be serviced by the one piece of hardware we are accessing. Once the hardware was successfully accessed, we would signal completion by calling the Signal() method of ISynchronize, and wait for the Finish_XXX method to be invoked to return the results.

The first step toward implementing server-side asynchrony is to support ICallFactory on your object. This has the method CreateCall, shown earlier, and our goal is to return an implementation of our asynchronous interface—our call object CAsyncFoo.

Listing 8.

The implementation of our call object is where most of the work will be done. First of all, this object must be aggregatable, because when CreateCall is invoked to create a call object, COM will always pass in a controlling outer as the second parameter. This is how the ISynchronize interface is slapped onto your call object. In this object, we need to keep track of where in the calling sequence we are, what the parameters were to the method, and what the results of the call are. Our implementation of Begin_DoFoo() will cache the long parameter, place our object in a queue of some type, and return, thus freeing up the RPC thread on which the method call was made. Our implementation of Finish_DoFoo() would take the cached results and place them in the out parameters, completing the call.

Listing 9.

The last piece of relevant code is the hardware queue itself. When the hardware picks one of our CAsyncFoo objects from its queue to service, it must fill its return value up, set its call state to done, and signal call completion by invoking the ISynchronize::Signal() method. Assuming that GetNextFoo() returns the next CAsyncFoo object from a queue, the code for our queue might look something like

Listing 10.

It is important to note that the server being able to handle the method call asynchronously is completely orthogonal to how the client invoked the call. Clients and servers can decide independently whether they want their call to be asynchronous, and the proxy/stub layer will accommodate their request. The client indicates asynchronous method invocation by constructing a call object and calling the Begin_XXX method. A server indicates asynchronous method execution by implementing ICallFactory and supplying its own call object to implement its asynchronous interface.

CONCLUSION Earlier versions of COM did not natively support nonblocking method invocation. COM+ introduces a flexible model for asynchronous calls that allows clients and servers to decide independently whether they want to handle a particular call asynchronously. Most of the asynchrony is handled by the proxy and stub placed between an object and its client, easing the burden on developers who desire nonblocking calls. Unfortunately, objects that are deployed as configured components (through the component services explorer) do not support asynchronous invocation. This type of asynchronous invocation also requires that the server and client be running simultaneously. For a more generic, message-based asynchrony that does not have this constraint, you may want to look at Microsoft Message Queue (MSMQ) and queued components.