Freigeben über


This article may contain URLs that were valid when originally published, but now link to sites or pages that no longer exist. To maintain the flow of the article, we've left these URLs in the text, but disabled the links.

MIND

 

Windows 2000: Asynchronous Method Calls Eliminate the Wait for COM Clients and Servers

Jeff Prosise

This article assumes you're familiar with C++ and COM
Level of Difficulty    1   2   3 
Code for this article: COMAsync0400.exe (133KB)

SUMMARYWindows 2000 is the first version of COM to support asynchronous method calls, which permit clients to make nonblocking calls to COM objects and objects to process incoming calls without blocking the calling threads. COM clients benefit from asynchronous method calls because they can continue working while waiting for outbound calls to return. Objects benefit because they can queue incoming calls and service them from a thread pool. Our SieveClient and SieveServer sample apps demonstrate how to create and use asynchronous clients and servers in COM-based distributed applications.

W

ith the release of Windows® 2000, developers can begin taking advantage of its new features in the applications they write. For COM developers, one of the most exciting new features is asynchronous method calls, which are part of the revamped COM infrastructure of Windows 2000. Clients that use asynchronous method calls don't block while waiting for calls placed to COM objects to return. Objects that use asynchronous method calls are no longer bound to process calls as they arrive, but can queue them up and process them when they please. Asynchronous method calls won't benefit every COM client and server, but they're a boon to clients that need to continue working while waiting for method calls to return, and to servers that want to accommodate concurrent requests with maximum efficiency.
      COM has been architected to make asynchronous method calls as simple as possible. Most of the dirty work is done in the MIDL-generated interface proxies and stubs, and in the channel that links them together. As is so often the case in COM, however, the devil is in the details. For example, it's trivial to write a client that places a nonblocking method call to a COM object. However, if that client wants to be notified when the call returns, it must implement a call object that aggregates a COM-provided call object. A server that processes calls asynchronously must implement an in-proc call object of its own and allow that object to be aggregated by COM. When it comes to implementing a client or server that utilizes asynchronous method calls, a clear explanation of the rules plus sample code demonstrating the principles involved are worth their weight in gold.
      What asynchronous method calls are, how they work, and how to use them is exactly what this article is all about. I'll begin with an overview of clients that perform nonblocking calls, then proceed to asynchronous servers. As you read, keep in mind that clients and servers are entirely independent of each other regarding asynchronous operation. In other words, an object does nothing special to let a client place nonblocking calls to it, and a client must do absolutely nothing to allow an object to process its method calls asynchronously. How a client makes a call determines whether that call is blocking or nonblocking, and how an object responds to incoming calls determines whether those calls are processed synchronously or asynchronously. The entity at the other end of the wire doesn't influence matters in the least.

Asynchronous Clients

      A key element of COM's support for asynchronous method calls is a new version of MIDL that supports a new IDL interface attribute named [async_uuid]. When it sees this attribute in an interface definition, MIDL generates both synchronous and asynchronous versions of the interface as well as interface proxies and stubs that support asynchronous method calls.
      Suppose you define an interface named ISieve that includes a method named CountPrimes, and that the CountPrimes method is defined as follows:

HRESULT CountPrimes ([in] unsigned long lMax,
                     [out] unsigned long* plResult);

Assume that clients can call CountPrimes to compute the number of prime numbers between 2 and a caller-specified ceiling called lMax. Counting prime numbers is a CPU-intensive operation that can take a long time to perform, so ISieve is an ideal candidate to be cast as an asynchronous interface.
      ISieve's IDL interface definition is shown in Figure 1. Because of the [async_uuid] attribute, MIDL will generate proxies and stubs that support nonblocking method calls. In addition, MIDL will define both a synchronous version of the interface named ISieve (interface ID=IID_ISieve) and an asynchronous version named AsyncISieve (interface ID=IID_AsyncISieve). The AsyncISieve interface will contain not one CountPrimes method, but two. One will be named Begin_CountPrimes, and will include all of CountPrimes's [in] parameters:

HRESULT Begin_CountPrimes ([in] unsigned long lMax);

The other will be named Finish_CountPrimes, and will include all of CountPrimes's [out] parameters:

HRESULT Finish_CountPrimes ([out] unsigned long* plResult);

      Because MIDL generates both synchronous and asynchronous versions of the interface, clients can still call the blocking version of CountPrimes by querying the object for an ISieve interface pointer and calling ISieve::CountPrimes. But now they have the added option of calling the nonblocking version of CountPrimes by calling AsyncISieve::Begin_CountPrimes followed by AsyncISieve:: Finish_CountPrimes. If the interface contains additional methods, Begin and Finish versions of those methods will be available, too. Also, note that if a method contains [in,out] parameters, those parameters will appear in both the Begin and Finish versions of that method. Specifically, [in,out] parameters will appear as [in] parameters in the Begin method and as [out] parameters in the Finish method.
      The object that implements the ISieve interface implements CountPrimes, but COM implements Begin_CountPrimes and Finish_CountPrimes. To be more precise, these methods, and the interface to which they belong, are implemented by a call object that is created by COM's proxy manager. The call object's purpose is to manage asynchronous method calls emanating from a COM client. Once the call object is created, a client can initiate a nonblocking call to CountPrimes by querying the call object for an AsyncISieve interface pointer and calling AsyncISieve::Begin_ CountPrimes. Call objects support just one outgoing call at a time, so if you want to execute two or more nonblocking calls in parallel, you must create a separate call object for each call. Once a call has returned, it's perfectly legal to make subsequent nonblocking calls through the same call object. If you want to make multiple nonblocking calls, but don't intend to call them in parallel, you can create one call object and use it over and over again.
Figure 2 The Client-side Architecture of Nonblocking Calls
Figure 2The Client-side Architecture of Nonblocking Calls

       Figure 2 shows the client-side architecture of nonblocking calls. The proxy manager creates and manages the interface proxies used to place calls to remote objects. It also implements ICallFactory. To obtain a pointer to the proxy manager's ICallFactory interface, a client simply queries for it through any of the interface pointers it holds on the object. The client then calls ICallFactory::CreateCall. Inside that call, the proxy manager creates a call object that in turn creates and aggregates an inner call object that implements AsyncISieve. Though not shown in Figure 2, the ISieve interface proxy implements an ICallFactory interface of its own that the proxy manager's call object calls to create the inner call object. For this to work, the ISieve interface must be defined with an [async_uuid] attribute. The proxy manager's call object also implements ISynchronize and ICancelMethodCalls. The purpose of these interfaces will be discussed shortly.

Executing a Nonblocking Call

      Given the architecture described in Figure 2, the procedure for executing a nonblocking method call to CountPrimes can be summed up as follows:

  1. Create the object that implements ISieve using CoCreateInstance[Ex] or another standard activation mechanism.
  2. Query the object (in reality, the proxy manager) for an ICallFactory interface.
  3. Call ICallFactory::CreateCall to create a call object that supports the AsyncISieve interface.
  4. Call AsyncISieve::Begin_CountPrimes on the call object to initiate a nonblocking call.
  5. Later, call AsyncISieve::Finish_CountPrimes on the call object to retrieve the call's output parameters.

       Figure 3 demonstrates this procedure, with error checking omitted for clarity. The client creates a sieve object, and receives an ISieve interface pointer in return. Then it queries for an ICallFactory interface pointer and calls ICallFactory::CreateCall to create a call object that implements AsyncISieve. Finally, it initiates a nonblocking call by calling Begin_CountPrimes, and completes it by calling Finish_CountPrimes. Between the call to Begin_CountPrimes and Finish_CountPrimes, the caller is free to go about its business. This is true because even though the call is still pending in the channel, the call to Begin_CountPrimes returns immediately. A SUCCEEDED return code from Begin_CountPrimes indicates only that the call has been initiated successfully. Even if the object ultimately returns an error code, that code won't be available when Begin_CountPrimes returns. Instead, it will show up as the HRESULT returned by Finish_CountPrimes.
      What happens under the hood when Begin_CountPrimes is called? First, the call object forwards the call to the channel. If the call is destined for another machine, the channel executes an asynchronous RPC call and provides the RPC subsystem with the address of a callback function. Once the RPC call is dispatched, the call to Begin_CountPrimes returns, freeing the caller's thread. When the asynchronous RPC call completes, the callback function is called. The callback function then signals the call object that the call has returned. When the client calls Finish_CountPrimes, the call object reaches into the channel and retrieves the returned output parameters.
      Remember that it's important to pair every call to a Begin method with a call to the corresponding Finish method. Among other things, COM allocates memory to hold the call's output parameters when Begin is called. If Finish isn't called, that memory won't be freed until the call object is released.

Checking for Call Completion

      If a nonblocking call returns before Finish is called, Finish returns immediately and returns the call's output parameters. If a nonblocking call has not returned when Finish is called, Finish blocks until it does. Calling Finish_CountPrimes immediately after Begin_CountPrimes returns is similar to performing a blocking method invocation.
      To prevent Finish_CountPrimes from blocking, the client might want to check the status of the method call before calling Finish. It can do so by calling ISynchronize::Wait on the call object through which the call was initiated. Calling ISynchronize::Wait is analogous to calling WaitForMultipleObjects to block on an event. Internally, Wait calls the new CoWaitForMultipleHandles API function in Windows 2000, which blocks differently depending on whether the calling thread is a single-threaded apartment (STA) thread or multithreaded apartment (MTA) thread. Because Wait accepts a timeout value, a call to it blocks for only as long as you allow it to. Moreover, Wait's return value identifies the status of the call. A return value equal to RPC_S_CALLPENDING means the call hasn't returned (and that right now a call to Finish would block); S_OK indicates that the call has returned.
       Figure 4 demonstrates how to determine the status of an outbound call, once again with error checking omitted. The assumption going in is that pAsyncSieve holds a pointer to the call object's AsyncISieve interface. That interface pointer is used to query the call object for an ISynchronize pointer, which in turn is used to call ISynchronize::Wait. The second 0 in Wait's parameter list means that Wait should return immediately, even if the call hasn't returned. If desired, you can pass a timeout value (in milliseconds) in this parameter and wait for up to that amount of time for the call to return.

Call Completion Notifications

      Pinging the call object from time to time to determine when a method call returns is an acceptable solution for some clients, but others might prefer to avoid polling. Fortunately, COM provides a mechanism that enables a caller to receive an asynchronous notification the moment a nonblocking method call returns.
      When a nonblocking call returns from the channel, the proxy notifies the call object by calling its ISynchronize::Signal method. Let's say the caller provides a call object of its own (the outer call object) that aggregates the proxy manager's call object (the inner call object). If the outer call object satisfies QueryInterface calls that are asking for ISynchronize interface pointers by returning pointers to its own ISynchronize interface, then the outerâ€"not the innerâ€"call object's Signal method will be called when a nonblocking method call returns.
      That's precisely the architecture depicted in Figure 5. The caller creates a call object that aggregates the system's call object. Aggregation is accomplished by passing a controlling unknownâ€"a pointer to the outer object's IUnknown interfaceâ€"to ICallFactory::CreateCall in that function's second parameter. When a nonblocking call returns, the outer object's ISynchronize::Signal method is called. It responds by calling the inner object's Signal method, and then uses a callback function, a Windows message, or some other mechanism to notify the caller that the call has returned. To avoid having to implement the ISynchronize methods Wait and Reset, the outer call object typically delegates handling of those methods to the inner call object. If asked for pointers to interfaces other than ISynchronize, the outer object blindly delegates those QueryInterface calls to the inner object to ensure that the aggregated call object's ICallFactory and ICancelMethodCalls interfaces remain accessible to callers.
Figure 5 Aggregating the Call Object
Figure 5Aggregating the Call Object

      In a moment, I'll show you a call object that was written in ATL. It aggregates the proxy manager's call object and notifies (using Windows messages) clients that a nonblocking method call has returned.

Canceling a Nonblocking Call

      Once it initiates a nonblocking method call, a client has no control over how much time elapses before the call returns. In some situations, however, a client might want to cancel a call if it hasn't returned after a specified period of time. That's why call objects implement an interface named ICancelMethodCalls. A client waiting for an outbound call to return can cancel the call by querying the call object for an ICancelMethodCalls pointer and calling ICancelMethodCalls::Cancel, as shown here:

pCancelMethodCalls->Cancel (0);

The lone parameter passed to Cancel specifies the number of seconds (not milliseconds) that Cancel will wait for the call to return after submitting a cancellation request. A timeout value of 0 issues the request and returns immediately. For proper cleanup, a client should call Finish after calling Cancel:

pCancelMethodCalls->Cancel (0);
unsigned long ulCount;
pAsyncSieve->Finish_CountPrimes (&ulCount);

In general, a client should ignore the output parameters that Finish returns following a call to Cancel because the server might have interrupted its processing of the method call to honor the cancellation request.
      Notice that I said Cancel might have interrupted the method call. A server doesn't respond to cancellation requests unless it is specifically designed to do so. Finish returns immediately if it's called after Cancel is called, regardless of whether the server honored the cancellation request. But a server that doesn't check for cancellation requests wastes CPU time if it continues to grind away on a method call after a client has indicated that it is no longer interested.
      Cancel returns S_OK to indicate that a cancellation request was submitted successfully. It returns RPC_E_CALL_COMPLETE if the call returns during the timeout period or had already returned when Cancel was called. Finish returns the method's HRESULT if the call returns before Cancel is called, or "0x8007171A (The remote procedure call was canceled)" if it doesn't.
      What does a server do to detect cancellation requests? It first calls CoGetCallContext to retrieve an ICancelMethodCalls pointer referencing a server-side call object. Then it calls ICancelMethodCalls::TestCancel to find out if a cancellation request has been submitted. TestCancel returns RPC_S_CALLPENDING if the call hasn't been cancelled, or RPC_E_CALL_CANCELED if it has. The more frequently the server calls TestCancel, the quicker it responds to cancellation requests. Once a cancellation request is detected, it's the server's responsibility to perform any necessary cleanup and to return from the method call as soon as possible.
       Figure 6 and 7 show two implementations of ISieve::CountPrimesâ€"one that responds to call cancellations (Figure 6) and one that does not (Figure 7). Both count prime numbers using the Sieve of Eratosthenes algorithm. The only difference between the two implementations is that the one in Figure 6 periodically calls TestCancel to determine whether a cancellation request is pending. If the answer is yes, CountPrimes breaks out of the nested for loop where the bulk of the computational work is done and performs an early exit.
      A client can also cancel a pending call by releasing the call object that was used to initiate the call. This technique frees the client from the obligation to call Finish, and automatically sends a cancel request to the server. The only potential drawback is that a released call object can't be used to make subsequent nonblocking calls. Additional calls using the same asynchronous interface will require the creation of a new call object.

SieveClient

      The SieveClient application shown in Figure 8 is an MFC COM client that demonstrates the techniques I've discussed for initiating, finishing, canceling, and checking the status of nonblocking method calls. To see SieveClient in action, start it and enter a number (say, 10,000,000 or 20,000,000) in the box in the upper-right corner of the window. Then click Begin to have SieveClient compute the number of prime numbers between 2 and the number you just entered. SieveClient uses a sieve object provided in a separate project to perform the computation. It calls the sieve object in one of three ways, depending on which of the three radio buttons in the bottom half of the window is selected.
Figure 8 SieveClient in Action
Figure 8SieveClient in Action

      If the Synchronous button is checked, SieveClient performs a normal blocking call by calling ISieve::CountPrimes on the sieve object. You can verify that the call is a blocking call by attempting to move the SieveClient window while waiting for the call to return. Because the thread that made the call is blocked in the channel, and because that thread is the application's one and only thread, SieveClient won't respond to user input until the call returns.
      Second, checking the Asynchronous button causes SieveClient to perform a nonblocking call by creating a call object and calling AsyncISieve::Begin_CountPrimes. Observe that SieveClient remains responsive to user input while waiting for the call to return. To complete the call and display the results, click the Finish button, which calls AsyncISieve::Finish_CountPrimes. If the call hasn't returned when you click Finish, Finish_ CountPrimes will block. You can find out whether the call has returned with the Get Call Status button, which calls ISynchronize::Wait. Or you can cancel the call with the Cancel button, which calls the call object's ICancelMethodCalls::Cancel method.
      The third option is to check "Asynchronous with automatic notification" before clicking Begin. Used this way, the client uses a call object to perform a nonblocking call. It also aggregates the call object with a call object of its own. The aggregating call object, which I'll refer to as the "call notify object," notifies SieveClient when the call returns by posting a message. SieveClient responds by calling Finish_CountPrimes and displaying the result. The code that posts the message is found in the call notify object's implementation of ISynchronize::Signal:

PostMessage (m_hWnd, m_nMessageID, 0, 0);

      How does the call notify object obtain the window handle passed to PostMessage? It implements a custom interface named ICallObjectInit that has a single method named Initialize. One of the parameters to that method is a window handle. To execute a nonblocking call accompanied by an asynchronous completion notification, the client creates the call notify object and passes its own window handle to ICallObjectInit::Initialize. Inside Initialize, the call notify object caches the window handle and aggregates the system-supplied call object by calling CreateCall through the ICallFactory interface pointer supplied by the client. After Initialize returns, the client calls AsyncISieve::Begin_CountPrimes on the call notify object.
      For the relevant source code, see SieveClient's CSieveClientDlg:: DoAsyncCallWithNotification function in SieveClientDlg.cpp, and the call notify object's implementation of ICallObjectInit:: Initialize in CallNotifyObject.cpp. You can download the source code for the client and the call notify object from the link at the top of this article. The client is in a project named SieveClient. The call notify object lives in a separate project named CallObjectServer, which is included in the downloadable files.
      The call notify object is an in-proc COM object implemented with the help of ATL. The statements

  COM_INTERFACE_ENTRY(ISynchronize)
  COM_INTERFACE_ENTRY_AGGREGATE_BLIND(m_spUnkInner.p)

in its interface map return a pointer to the call notify object_'s own ISynchronize interface in response to QueryInterface calls, and delegate QueryInterface calls for other interfaces to the aggregated call object. Internally, the call notify object caches a pointer to the inner object's ISynchronize interface and uses it to call the inner object's ISynchronize methods.
      To experiment with SieveClient, you must first build the projects containing the call notify object and the sieve object. The latter is in a project named SieveServer, which is also available from the link at the top of this article. Both projects, when built, register the COM objects contained inside them as well as the requisite proxy/stub DLLs. SieveServer is an asynchronous server, but that's irrelevant to SieveClient. Clients can execute nonblocking calls regardless of whether the objects targeted by those calls operate synchronously or asynchronously.

Asynchronous Servers

      COM's nonblocking method call infrastructure supports asynchronous processing on the server side as well as on the client side. A chief limitation of a conventional COM server is that when a call enters the server process, the calling thread will be blocked for the duration of the method call. An asynchronous server dispatches method calls to other threads and permits the calling threads to return immediately.
      While there's nothing to prevent any COM server from spinning up threads to process method calls, Windows 2000 offers a formal architecture for freeing the calling thread and signaling the stub when processing is complete. A typical asynchronous server will maintain a pool of threads that it uses to process incoming calls. As calls come in, they're placed in a queue where they wait to be picked up by a thread from the thread pool.
      By relying on a finite-sized thread pool rather than allocating a new thread for each call, a server can process incoming requests more efficiently when the number of concurrent calls significantly exceeds the number of available CPUs. Moreover, an asynchronous server written the Windows 2000 way can process calls asynchronously without requiring any special actions on the part of the client. This means that the client can make a normal (blocking) call to ISieve:: CountPrimes, but if the sieve object is an asynchronous server, then its CountPrimes method won't be calledâ€"the call will go to Begin_CountPrimes and Finish_CountPrimes instead.
      The only requirement for an object that wants to process method calls asynchronously is that it must implement ICallFactory, and it must respond to calls to ICallFactory::CreateCall by creating a server-side call object that implements both the asynchronous interface identified in CreateCall's parameter list and ISynchronize. The call object accompanying an asynchronous sieve object, for example, would implement AsyncISieve and ISynchronize. The presence of ICallFactory alerts the stub to translate calls to the CountPrimes method on the sieve object into calls to Begin_ CountPrimes and Finish_CountPrimes on the call object. The stub creates the call object when the first call arrives by querying the sieve object for an ICallFactory interface pointer and then calling ICallFactory::CreateCall.
      Since the call object provided by the server implements the asynchronous interface's Begin and Finish methods, the object implementor controls what happens inside those methods. A Begin method transfers the call to another thread, either by queuing it for retrieval by a pooled thread or by creating a new thread, and then returns. When the thread that processes the call is finished, it signals the system that processing is complete by calling ISynchronize::Signal on the call object. This prompts the system to call Finish, after which the original method call executed by the client finally returns. For its part, the call object's Finish method does little more than return the output parameters generated by the thread that processed the call, along with an HRESULT indicating whether the call succeeded or failed.

Implementing the Call Object

      Your chief responsibility when writing an asynchronous server is implementing the server-side call object. In the sample presented in the next section, I use a private ATL COM class to represent the call object. It's designed for internal in-proc use only, has no class object, and doesn't support external activation. The bulk of the work lies in implementing the asynchronous interface's Begin and Finish methods and the thread functions used to process the calls. However, there's also the matter of implementing ISynchronize. Fortunately, the system provides an implementation of ISynchronize that you can borrow by aggregating the object whose CLSID is CLSID_ManualResetEvent and delegating QueryInterface calls asking for ISynchronize pointers to the aggregatee. In ATL, you can accomplish this in one step by including the statement

CComPtr<IUnknown> m_spUnkInner;

in the class declaration and the statement

COM_INTERFACE_ENTRY_AUTOAGGREGATE (IID_ISynchronize,
                                   m_spUnkInner.p, 

                                   CLSID_ManualResetEvent)

in the interface map.
      Another point to remember is that the call object you write must be an aggregatable object. When the stub creates your call object, it aggregates the object with a system-supplied call object whose IUnknown is provided in the second parameter to CreateCall. The purpose of the aggregation is to allow COM to contribute a few interfaces of its own to the call object, including an ICancelMethodCalls interface that the server can use to detect cancellation requests (see Figure 9). Observe that the outer object also implements ISynchronize. Your ISynchronize implementation is used only if your call object doesn't get aggregated by COM. This only happens if the thread that calls ICallFactory::CreateCall resides in the same apartment as the object that is the target of the call.
Figure 9 Detecting Cancellation Requests
Figure 9Detecting Cancellation Requests

      Normally, it's the stub that calls the Begin and Finish methods on the server-side call object. But because an in-proc client could call these methods directly, you should build the following safeguards into your Begin and Finish methods. First, because call objects support only one call at a time, have Begin reject the call by returning RPC_S_CALLPENDING if it's called while the call object is processing another method call. This typically means maintaining a flag that Begin can check to determine whether a call is in progress. Second, have your Finish methods reject calls that aren't preceded by calls to the corresponding Begin methods. The proper HRESULT to return, should this occur, is RPC_E_CALL_COMPLETE. Finally, have your Finish methods call ISynchronize::Wait on the call object to be absolutely sure that Finish won't return bogus results if it's called before the thread that processes the call has completed its work.
      Another implementational detail to keep in mind is that if a method call fails in Begin or in the thread that processes the call, and you want to return an HRESULT to the caller indicating why the call failed, you must return that HRESULT from the Finish method. One way to accomplish that is by storing the HRESULT that Finish returns in the call object and making it accessible to the Begin method and the thread that processes the call.

SieveServer

      The sample code for this article includes SieveServer, an ATL COM server that houses a sieve object that processes method calls asynchronously. Sieve.h and Sieve.cpp contain the source code for the sieve object. It is unusual in that its implementation of ISieve:: CountPrimes returns E_NOTIMPL. Here CountPrimes will never be called unless the stub's call to ICallFactory::CreateCall fails, in which case the stub will use synchronous method calls as a fallback.
      To support asynchronous processing on the server side, the SieveServer sieve object implements ICallFactory. The stub calls the object's ICallFactory::CreateCall method to create a server-side call object. The call object is created (and aggregated) by the following statements in CreateCall:

CComPolyObject<CServerCallObject>* pCallObject = NULL;
HRESULT hr = CComPolyObject<CServerCallObject>
    ::CreateInstance (pUnk,&pCallObject);

CServerCallObject is the ATL class that implements the call object. Wrapping it in ATL's CComPolyObject class makes the resulting COM object an aggregatable object, and CComPolyObject::CreateInstance instantiates the object using proper ATL semantics.
      CServerCallObject's source code is found in ServerCallObject.h and ServerCallObject.cpp. Its Begin_CountPrimes method uses the new QueueUserWorkItem function in Windows 2000 to dispatch a worker thread and pass it a this pointer. The worker thread uses the this pointer to retrieve the input parameter IMax from the call object. It then performs the primes computation, copies output parameters to the call object, and signals the stub that the call is complete. The stub responds by calling Finish_CountPrimes, which returns the output parameters to the caller. If a failure occurs in Begin_CountPrimes, a failed HRESULT is copied to the call object's m_hResultFinish and returned by Finish_CountPrimes. These acrobatics are necessary if a failure in any part of the mechanism that processes the method call is to be propagated back to the caller.
      QueueUserWorkItem is part of the new thread pooling API that can greatly ease the chore of adding thread pooling to asynchronous COM servers. For more information on thread pooling, see Jeffrey Richter's article, "New Windows 2000 Pooling Functions Greatly Simplify Thread Management," in the April 1999 issue of Microsoft Systems Journal (https://www.microsoft.com/msj/0499/pooling/pooling.htm).
      One aspect of this server's architecture that merits further discussion is how the threads that calls are dispatched on acquire their ISynchronize interface pointers. SieveServer is an MTA-based COM server, which means that the objects it creates run in the process's MTA. (It doesn't make sense to put an object that processes method calls asynchronously in an STA because calls to STA-based objects are serialized before they reach the stub.) Because ThreadFuncâ€"the thread function that's executed when a worker thread is dispatched by QueueUserWorkItemâ€"calls CoInitializeEx with a COINIT_MULTITHREADED parameter, worker threads also run in the MTA. Since call objects and their worker threads share an apartment, Begin_CountPrimes doesn't have to marshal ISynchronize interface pointers to its worker threads. Instead, the threads simply call QueryInterface through the this pointer provided to them by Begin_CountPrimes.
      Why is that important? The outer call object's ISynchronize interface is not apartment-neutral, which means that if for some reason you write a Begin method that dispatches calls to threads in other apartments, Begin must marshal an ISynchronize interface pointer to those threads. To prove it, temporarily change

CoInitializeEx (NULL, COINIT_MULTITHREADED);

n ThreadFunc to read

CoInitializeEx (NULL, COINIT_APARTMENTTHREADED);

then rebuild the server and use SieveClient to execute an asynchronous call. Be prepared to wait a while because the call will never return.
      SieveServer's sieve object doesn't check for cancellation requests, so if the client cancels, the thread that's processing the call in the server continues to run, blissfully unaware that the call has been canceled and that any output it returns will be ignored. You can fix that by modifying the thread function to call ICancelMethodCalls::TestCancel periodically and perform an early exit if TestCancel returns RPC_ E_CALL_CANCELED. Rather than retrieve an ICancelMethodCalls interface pointer by calling CoGetCallContext, have the thread function get the pointer by calling QueryInterface on the call object. The pointer doesn't have to be marshaled if the call object and its helper threads share an apartment. But if they're in separate apartments, marshal the interface pointer to transport it safely across apartment boundaries.

Canceling a Blocking Call

      You now have seen how to cancel asynchronous calls from the client side, and how to respond to cancellation requests on the server side. But did you know that in Windows 2000, COM also permits callers to cancel synchronous (blocking) calls? It does. Here's how it works.
      A thread that's waiting for a blocking call to return can't cancel the call itself because it's blocked, waiting for the call to return. However, another thread can cancel the call on the calling thread's behalf. First, the other thread calls CoGetCancelObject and asks for an ICancelMethodCalls interface pointer. Then it calls ICancelMethodCalls::Cancel, as shown here:

ICancelMethodCalls* pCancelMethodCalls;
CoGetCancelObject (nThreadID, IID_ICancelMethodCalls,
                   (void**) &pCancelMethodCalls);

pCancelMethodCalls->Cancel (0);
pCancelMethodCalls->Release ();

Note that CoGetCancelObject requires a thread ID. That ID identifies the thread that placed the blocking call.
      Be aware that the ability to cancel synchronous calls is disabled by default and must be enabled on a per-thread basis if it is to work. A thread enables call cancellation by calling CoEnableCallCancellation. Once enabled for a given thread, call cancellation can be disabled again by calling CoDisableCallCancellation. Both functions must be called by the thread that places the synchronous calls to a COM object; one thread can't call these functions on another thread's behalf. Enabling this feature can measurably degrade the performance of synchronous method calls, so don't enable it unless you intend to use it.

Cross-platform Considerations

      Asynchronous method calls are new in Windows 2000 and therefore require Windows 2000 to work. However, it's possible for COM clients running on Windows 2000 to make asynchronous calls to COM servers on machines running Windows NT® 4.0. It's also possible for COM servers running on Windows 2000 to asynchronously process method calls emanating from clients running on Windows NT 4.0. The secret is to use two different proxy/stub DLLs.
      To make nonblocking calls from a machine running Windows 2000 to a machine running Windows NT 4.0, you can compile two versions of the proxy/stub DLL containing the marshaling code for the server's interfaces: an asynchronous version in which the interface is marked with an [async_uuid] attribute, and a synchronous version compiled without [async_uuid]. Install the asynchronous proxy/stub DLL on the machine running Windows 2000, and the synchronous proxy/stub DLL on the machine running Windows NT 4.0. The client for Windows 2000 can now make nonblocking method calls to the server for Windows NT 4.0.
      If the scenario is reversed and you have Windows NT 4.0-based clients placing calls to Windows 2000-based servers, you can enable asynchronous processing on the server side by once more compiling two versions of the proxy/stub DLL. This time, install the synchronous version on the client machines (Windows NT 4.0) and the asynchronous version on the server machines (Windows 2000). Presto! Calls will block on the client side, but the server will be free to process them asynchronously.

Restrictions and Limitations

      Before you run off to add asynchronous processing support to a COM client or server, be aware of the following limitations.
      First, as stated previously, call objects support just one outgoing call at a time. To execute two or more calls in parallel, use multiple call objects. Be aware that if two or more calls are placed in parallel, the order in which they arrive at the server is not guaranteed to be the order in which they were made. For example, if a client calls Begin_Foo and then Begin_Bar, the server's Foo method may or may not be called before its Bar Method.
      Second, asynchronous method calls are dependent upon proxy/stub DLLs compiled with [async_uuid] attributes and therefore can't be used with type library marshaling. Asynchronous calls are also incompatible with IDispatch interfaces and duals. MIDL will reject an [async_uuid] attribute applied to an IDispatch interface or any interface that derives from IDispatch. In other words, use custom interfaces only, and forget about type library marshaling. The only situation in which a custom proxy/stub DLL isn't required is when a client creates an in-proc, in-apartment component that implements ICallFactory. Even without a proxy/stub DLL, that client can use ICallFactory::CreateCall to create a call object and then execute asynchronous calls by calling the call object's Begin and Finish methods. Call cancellation won't work in this scenario, however, unless the server-provided call object implements ICancelMethodCalls.
      Finally, COM doesn't support asynchronous calls to configured componentsâ€"components that are included in COM+ applications and are registered as such in the COM+ catalog. As a workaround, you can place asynchronous calls to nonconfigured wrapper components that then forward the calls to the configured components. For related articles see:
https://msdn.microsoft.com/library/techart/nbmc.htm
Background information:
https://msdn.microsoft.com/library/psdk/com/com_757w.htm
Microsoft Interface Definition Language (MIDL) https://msdn.microsoft.com/library/psdk/midl/midlstart_4ox1.htm
Also checkInside Distributed COM, Guy Eddon and Henry Eddon (Microsoft Press)

Jeff Prosise is the author of Programming Windows with MFC (Microsoft Press, 1999). He also teaches Visual C++, MFC, and COM programming seminars. For more information, visit https://www.prosise.com.

From the April 2000 issue of MSDN Magazine.