. |
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:
- A way to indicate that a call is being made asynchronously;
- A polling or callback mechanism to know when the call is complete; and
- 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.
|