Page #117 (Object Pooling)

< BACK  NEXT >
[oR]

Asynchronous Method Calls

Under the conventional COM+ model, when a client application calls a method on an object, the client thread blocks until the method executes and returns. Making such a synchronous call is by far the most popular technique to the clients. However, there are times when a client application would want to do some other work while the call is in progress, rather than waiting for the call to complete.

One way to accomplish this is for the client to spawn a worker thread to make the method call. The primary thread is now free to do some other work while the worker thread blocks until the method call returns.

Though this technique will certainly work, it is better if the underlying infrastructure provides this type of service. This reduces the thread management code for the client, especially if the client intends to make many concurrent calls. Moreover, the infrastructure can optimize the low-level calls to make it more performance-efficient.

The RPC layer under Windows 2000 includes support for asynchronous calls. COM+ leverages this feature to support asynchronous call processing at the interface method level. The architecture is such that both the client and the server can independently deal with asynchronous method calls:

  • The client code can be developed to make asynchronous method calls without requiring the server to implement any special code. The COM+ MIDL compiler can generate proxies and stubs that do all the dirty work.

  • The server code can process calls asynchronously without requiring any special action on the part of the client. As a matter of fact, the server can process a method call in an asynchronous fashion even if the client makes a synchronous method call. Once again, the MIDL-generated proxies and stubs do all the magic.

To explore COM+ support for asynchronous calls, we will develop a very simple server component. The component returns the sum of two numbers. Following is the interface definition used for the demonstration:

 interface IMySum : IUnknown  {   HRESULT GetSum([in] long lVal1, [in] long lVal2,      [out, retval] long* plSum);  }; 

The implementation of such an interface method is shown below. It is quite straightforward. To ensure that the method call execution takes some time, I have deliberately added a five-second sleep in the code:

 STDMETHODIMP CMySum::GetSum(long lVal1, long lVal2, long *plSum)  {   ::Sleep(5 * 1000); // sleep for 5 seconds    *plSum = lVal1 + lVal2;    return S_OK;  } 

In order for MIDL to generate proxy/stub code that deals with asynchronous method calls, the interface definition needs to be marked with an attribute named [async_uuid], as shown here:

 [   ...    uuid(1189F283-7248-43C7-988B-57397D67BAB5),    async_uuid(E58D142E-0199-4178-B93A-9F1919DE42D3),  ]  interface IMySum : IUnknown  {   HRESULT GetSum([in] long lVal1, [in] long lVal2,      [out, retval] long* plSum);  }; 

When MIDL sees the async_uuid attribute, it generates the proxy/stub code for two versions of the interface IMySum.

  1. A synchronous version of the interface, named IMySum, with interface ID taken from the uuid interface attribute. This is the normal MIDL processing that we are accustomed to.

  2. An asynchronous version of the interface, named AsyncIMySum, with interface ID taken from the async_uuid interface attribute.

The AsyncIMySum interface contains two methods for each method found in the IMySum interface. A Begin_ and Finish_ token, as shown below, prefixes each method:

 interface AsyncIMySum  {   HRESULT Begin_GetSum([in] long lVal1, [in] long lVal2);    HRESULT Finish_GetSum( [out, retval] long* plSum);  }; 

You can easily guess that a client would call the Begin_ version of the method to start an asynchronous method call, and the Finish_ version to obtain the outcome of the method call.

The coclass implements the IMySum interface as usual. Typically, it does not implement the asynchronous version of the interface. Instead, COM+ implements the AsyncIMySum interface and makes it available to the client.

Note that only the [in] parameters of the original method appear in the Begin_GetSum method and only the [out] parameters of the original method appear in the Finish_GetSum method. Had the original method contained any [in, out] parameter, it would appear as an [in] parameter in the Begin_ version, and as an [out] parameter in the Finish_ version.

Before we go further, there are a few general limitations concerning asynchronous support in COM+ you should be aware of:

  1. COM+ doesn t support asynchronous calls from configured components. As a workaround, you can make an asynchronous call to a non-configured component and have that component call the configured component.

  2. Asynchronous interfaces can only be derived from asynchronous interfaces. This rules out deriving interfaces from dispinterface or IDispatch. Interface IUnknown, as you might have guessed, is marked to support asynchronous calls, making it possible to define custom asynchronous interfaces.

  3. As asynchronous call support is implemented by the proxy/stub code, type library marshaling cannot be used. The proxy/stub DLL has to be built and registered.

  4. As the asynchronous magic happens because of the proxy/stub logic, a client cannot use a direct interface pointer to the object. A simple way to ensure that the client gets a proxy is to make the component an outof-process server.

  5. As the proxy/stub code is meant to execute only on Windows 2000 or later versions of the OS, the code compilation requires that a macro, _WIN32_WINNT, be defined as at least 0x0500 (for NT 5.0).

graphics/01icon02.gif

If you are using ATL to develop the server, be sure to pass FALSE as a parameter to the _Module.RegisterServer call. This will ensure that the type library does not get registered. Also, you need to edit the ATL wizard-generated proxy stub makefile and redefine the _WIN32_WINNT macro to 0x0500.


Let s look at how a client can make asynchronous calls.

Asynchronous Clients

First, we need to understand the client-side architecture of asynchronous calls.

As mentioned earlier in the section, the server object typically implements the synchronous version of the interface and COM+ implements the asynchronous version. More precisely, a separate object, known as a call object, implements the asynchronous version of the interface. A client can get hold of the call object through the proxy manager.

Recall from Chapter 5 that when a client activates an object from a context that is incompatible with that of the object s configuration, a proxy manager gets loaded in the client s context. The proxy manager acts as the client-side identity of the object.

Figure 11.5 illustrates the interfaces exposed by the proxy manager for our sample application.

Figure 11.5. Interfaces exposed by the proxy manager.
graphics/11fig05.gif

Besides exposing the interfaces supported by the actual object, the proxy manager also exposes some other interfaces. One such interface that we saw earlier in Chapter 7 is IClientSecurity. Another interface that the proxy manager implements, and the one that is of interest to us, is ICallFactory. Following is its definition:

 interface ICallFactory : IUnknown  {   HRESULT CreateCall(     [in] REFIID riid,      [in] IUnknown *pCtrlUnk,      [in] REFIID riid2,      [out, iid_is(riid2)]IUnknown **ppv );  } 

Method CreateCall lets the client create a call object.

Parameter riid lets you specify the identifier of the asynchronous interface you are interested in. An object may support more than one asynchronous interface. By specifying the asynchronous interface identifier, the call factory can create a call object appropriate for the specific asynchronous interface.

Parameter pCtrlUnk, if not NULL, is used for aggregating the call object. We will see its use later.

Besides supporting the asynchronous interface, a call object supports two more interfaces, ISynchronize and ICancelMethodCalls, as illustrated in Figure 11.6. Parameter riid2 specifies the identifier of the interface you wish to obtain on the call object.

Figure 11.6. Interfaces exposed by the call object.
graphics/11fig06.gif

Given this client-side architecture, the procedure for executing an asynchronous method call to GetSum can be summed up as follows:

  1. Create the object that implements IMySum, using CoCreateInstance, for example.

  2. Query the obtained proxy manager for the ICallFactory interface.

  3. Call ICallFactory::CreateCall to create a call object that supports the AsyncIMySum interface.

  4. Call AsyncIMySum::Begin_GetSum on the call object to initiate an asynchronous call.

  5. Later, call AsyncIMySum::Fetch_GetSum on the call object to retrieve the call s output parameters.

The following code fragment demonstrates this procedure. As usual, error checking has been omitted for clarity.

 void SimpleAsyncExecution()  {   // Step 1: Create the instance    CComPtr<IUnknown> spUnk;    HRESULT hr = ::CoCreateInstance(__uuidof(MySum), NULL,      CLSCTX_SERVER, __uuidof(IMySum), (void**) &spUnk);    // Step 2: Get the call factory    CComPtr<ICallFactory> spCallFactory;    hr = spUnk->QueryInterface(&spCallFactory);    spUnk = NULL; // not needed anymore    // Step 3: Get the async interface    CComPtr<AsyncIMySum> spMySum;    hr = spCallFactory->CreateCall(__uuidof(AsyncIMySum),      NULL,      __uuidof(AsyncIMySum),      (IUnknown**) &spMySum);    spCallFactory = NULL; // not needed anymore    // Step 4: Initiate the call    hr = spMySum->Begin_GetSum(10, 20);    // Step 5: Get the value    long lSum;    hr = spMySum->Finish_GetSum(&lSum);    cout << "Sum is: " << lSum << endl;    // Step 6: Clean up    spMySum = NULL;  } 

Note that the call to Begin_GetSum returns immediately, giving the caller a chance to do some other processing before calling Finish_GetSum.

A couple of things to keep in mind while using the call object:

  1. It is important to pair every call to the Begin method with a call to the corresponding Finish method. COM+ allocates memory to hold the call s output parameters when Begin is called. If Finish isn t called, this memory (and some other resources) is not freed until the call object is released.

  2. A call object supports only one outgoing call at a time. A client cannot call the Begin method a second time without calling the Finish method first. This is not really a problem. To execute multiple calls concurrently, the caller just has to create multiple call objects.

Under the hood, when Begin is called, the ORPC channel executes an asynchronous RPC call and provides the RPC subsystem with the address of a callback function. The Begin method then returns, freeing the caller s thread. When the asynchronous RPC call completes, it invokes the callback function. The callback function in turn signals the call object that the call has returned.

If Finish is called after the asynchronous RPC call is completed, the method returns immediately (with the call s output parameters). If the asynchronous RPC call is not completed when Finish is called, Finish will block until the call completes.

Checking Call Status

To prevent Finish from blocking, a client can check to see if the RPC call has completed. It can do so by calling ISynchronize::Wait on the call object. A return value of RPC_S_CALLPENDING indicates that the call has not been completed (and hence a call to Finish at the moment would block). The following code snippet demonstrates how to check for call completion:

 // Step 5: Check for call completion every one second  CComPtr<ISynchronize> spSync;  hr = spMySum->QueryInterface(&spSync);  _ASSERT (SUCCEEDED(hr));  for(;;) {   Sleep(1 * 1000);    hr = spSync->Wait(0, 0);    if (RPC_S_CALLPENDING != hr) {     // finished call completion. Get the value.      break;    }    cout << "Call is pending" << endl;  } 
Call Completion Notification

Polling from time to time is one way to check for call completion. COM also provides another way wherein the client can be notified asynchronously when the call completes. This is accomplished by a little trick.

When the asynchronous RPC call completes, COM notifies the call object by querying it for the ISynchronize interface and calling the Signal method on it. If the caller provides a call object of its own that supports ISynchronize and aggregates the system-supplied call object, then querying the call object for the ISynchronize returns a pointer to the caller-provided call object. The Signal method will then be called on this object instead of the system-supplied call object. The following ATL-based code shows an implementation of such a call object:

 class CMyCallNotify :    public ISynchronize,    public CComObjectRootEx<CComMultiThreadModel>  { public:    CMyCallNotify() {}    HRESULT Init(ICallFactory* pCallFactory);    ~CMyCallNotify() {}  DECLARE_GET_CONTROLLING_UNKNOWN ()  BEGIN_COM_MAP(CMyCallNotify)    COM_INTERFACE_ENTRY(ISynchronize)    COM_INTERFACE_ENTRY_AGGREGATE_BLIND (m_spUnkInner.p)  END_COM_MAP()  // ISynchronize  public:    STDMETHOD(Wait)(ULONG dwFlags, ULONG dwMilliseconds);    STDMETHOD(Signal)();    STDMETHOD(Reset)();  private:    CComPtr<ISynchronize> m_spSyncInner;    CComPtr<IUnknown> m_spUnkInner;  };  HRESULT CMyCallNotify::Init(ICallFactory* pCallFactory)  {   // Step 1: Create a call object.    HRESULT hr = pCallFactory->CreateCall(__uuidof(AsyncIMySum),      GetControllingUnknown(),      IID_IUnknown, &m_spUnkInner);    if (FAILED (hr))      return hr;    // Cache a pointer to the aggregated object's    // ISynchronize interface.    hr = m_spUnkInner->QueryInterface(__uuidof(ISynchronize),      (void**) &m_spSyncInner);    if (FAILED (hr)) {     m_spUnkInner = NULL;      return hr;    }    return hr;  }  STDMETHODIMP CMyCallNotify::Wait(ULONG dwFlags,    ULONG dwMilliseconds)  {   // Forward the call to the inner object    return m_spSyncInner->Wait(dwFlags, dwMilliseconds);  }  STDMETHODIMP CMyCallNotify::Signal()  {   // Forward the call to the inner object    HRESULT hr = m_spSyncInner->Signal();    // Notify the user    cout << "Call finished." << endl;    cout << "Press enter to fetch the sum" << endl;    return hr;  }  STDMETHODIMP CMyCallNotify::Reset()  {   // Forward the call to the inner object    return m_spSyncInner->Reset();  } 

Note that the outer call object only implements ISynchronize methods. If queried for any interface other than ISynchronize, the outer call object blindly delegates the call to the inner object.

Also note that, for proper functioning, it is important to delegate all the ISynchronize calls to the inner object. In particular, if Signal is not forwarded, a call to Finish_GetSum will block indefinitely.

Call Cancellation

Once a call is initiated, a client has no control over how long the call takes to finish. In some cases, the client may wish to cancel the call if it hasn t returned after a specified period of time. The call object supports another interface, ICancelMethodCalls, to do exactly this. The following code snippet shows its usage:

 // Cancel the call if the call takes more than a second  CComPtr<ICancelMethodCalls> spCancel;  hr = spMySum->QueryInterface(&spCancel);  _ASSERT (SUCCEEDED(hr));  spCancel->Cancel(1);  // Get the value  long lSum;  hr = spMySum->Finish_GetSum(&lSum); 

The parameter to ICancelMethodCalls::Cancel specifies the number of seconds Cancel should wait before canceling the call in progress. A value of 0 implies that the call in progress should be canceled right away.

Note that, for proper cleanup, a client should still call Finish after calling cancel. Of course, Finish will fail with an RPC_E_CALL_CANCELED error message. The client should just ignore the output parameters in this case.

When a client cancels a call in progress, the Finish method returns immediately with a RPC_E_CALL_CANCELED message. However, the server is not aware about the client-side cancelation and will continue to execute the method thereby wasting CPU time.

The server could be designed to detect cancelation. Recall from Chapter 7 that a server can obtain its call context by calling CoGetCallContext. This call context supports interface ICancelMethodCalls, which the server can use to check periodically if a call has been cancelled. The following code demonstrates this procedure:

 STDMETHODIMP CMySum::GetSum(long lVal1, long lVal2, long *plSum)  {   CComPtr<ICancelMethodCalls> spCancel;    HRESULT hr = ::CoGetCallContext(__uuidof(ICancelMethodCalls),      (void**) & spCancel);    for(int i=0; i<5;i++) {     ::Sleep(1 * 1000);      hr = spCancel->TestCancel();      if (RPC_E_CALL_CANCELED == hr) {        return RPC_E_CALL_CANCELED;      }    }    *plSum = lVal1 + lVal2;    return S_OK;  } 

Asynchronous Servers

With a conventional COM server, when a call enters the server process, the calling thread gets blocked until the call finishes its execution and returns.

If the call is expected to take a significant amount of time to execute, one can employ some clever techniques in the server code to free up the calling thread in a short time. For example, the server can spawn a thread and dispatch the method call to the thread, thus freeing up the calling thread immediately.

COM+, however, offers a formal architecture for developing asynchronous COM servers. An asynchronous server written the COM+ way processes the incoming call in two stages, Begin and Finish, much like the asynchronous method calls the clients made in the earlier section. If the client makes asynchronous method calls, the stub maps them to the server implementation of the method calls. In case the client makes a synchronous method call, the stub calls the Begin and Finish methods on the client s behalf.

In order to develop an asynchronous COM server, the following requirements have to be met:

  1. The coclass has to implement the ICallFactory interface.

  2. When the stub calls the ICallFactory::CreateCall method, the coclass implementation should respond by creating a server-side call object and returning it to the stub.

  3. The coclass need not implement the synchronous version of the interface. Even if it does, the stub never invokes the synchronous interface methods.

Following are the requirements for the server-side call object:

  1. The call object must be an aggregatable object. When the stub creates your call object, it aggregates the call object with a system-supplied call object whose IUnknown is provided in the second parameter to CreateCall.

  2. The server-side call object should implement the asynchronous version of the interface.

  3. The server-side call object should also implement the ISynchronize interface.

  4. The call object should ensure that only one call can be processed at a time. The Begin method should reject a second call by returning RPC_S_CALLPENDING if it is called while the call object is already processing another method call.

  5. If Finish is called without calling the corresponding Begin method first, Finish should reject the call by returning RPC_E_CALL_COMPLETE.

  6. If the processing of the call fails, the failure status should be returned as HRESULT when Finish is invoked.

Implementing the coclass is relatively straightforward. The following code snippet shows the implementation for a class that supports IMySum2. This interface is equivalent in functionality to IMySum that we used earlier.

 class CMySum2 :    public IMySum2,    public ICallFactory,    ...  {   ...  BEGIN_COM_MAP(CMySum2)    COM_INTERFACE_ENTRY(IMySum2)    COM_INTERFACE_ENTRY(ICallFactory)  END_COM_MAP()  // IMySum2  public:    STDMETHOD(GetSum)( long lVal1, long lVal2, long* plSum);  // ICallFactory    STDMETHOD(CreateCall)(REFIID riid1, IUnknown* pUnk,      REFIID riid2, IUnknown** ppv);  };  STDMETHODIMP CMySum2::CreateCall(REFIID riid1, IUnknown* pUnk,    REFIID riid2, IUnknown** ppv)  {   *ppv = NULL;    // Step 1: Ensure that input parameters are valid    if (__uuidof(AsyncIMySum2) != riid1) {     return E_INVALIDARG;    }    if (NULL != pUnk && IID_IUnknown != riid2) {     return E_INVALIDARG;    }    // Step 2: Create call object and aggregrate it with pUnk    CComPtr<CComPolyObject<CMyAsyncSumCallObject> > spCallObject;    HRESULT hr =      CComPolyObject<CMyAsyncSumCallObject>::CreateInstance(       pUnk, &spCallObject);    _  ASSERT (SUCCEEDED(hr));    spCallObject->InternalAddRef();    // Step 3: Return the requested interface    return spCallObject->QueryInterface(riid2, (void**) ppv);  } 

Implementing a call object requires implementing the ISynchronize and AsyncIMySum interfaces.

Instead of implementing your own ISynchronize interface, you can use one from the system-supplied object whose CLSID is CLSID_ManualResetObject. All you need to do is to aggregate the system-supplied object with your call object and delegate QueryInterface requests for ISynchronize to the aggregatee. In ATL, this can be done in one step by using the COM_INTERFACE_ENTRY_AUTOAGGREGATE macro. The class definition for our call object is shown here:

 class CMyAsyncSumCallObject :    public AsyncIMySum2,    public CComObjectRoot  { public:    CMyAsyncSumCallObject();    HRESULT FinalConstruct();    ~CMyAsyncSumCallObject();  DECLARE_GET_CONTROLLING_UNKNOWN ()  BEGIN_COM_MAP(CMyAsyncSumCallObject)    COM_INTERFACE_ENTRY(AsyncIMySum2)    COM_INTERFACE_ENTRY_AUTOAGGREGATE(IID_ISynchronize,      m_spUnkInner.p, CLSID_ManualResetEvent)  END_COM_MAP()  // AsyncIMySum2  public:    STDMETHOD(Begin_GetSum)(/*[in]*/ long lVal1,      /*[in]*/ long lVal2);    STDMETHOD(Finish_GetSum)(/*[out, retval]*/ long* plSum);    CComPtr<IUnknown> m_spUnkInner;  private:    friend class CMyThread;    CMyThread m_Thread;    long m_lVal1;    long m_lVal2;    long m_lSum;    typedef CCPLWinSharedResource<bool> SAFEBOOL;    SAFEBOOL m_bCallInProgress;  }; 

It should be noted that the system-supplied call object also implements the ISynchronize object. Your ISynchronize interface is used only if your call object is not aggregated by the stub. This only happens if your call object resides in the same apartment as the thread that calls ICallFactory::CreateCall.

In the above class definition, member variable m_bCallInProgress is used to make certain that only one Begin call is serviced at a time by the call object. As the Begin and Finish methods can be accessed from multiple threads, this variable needs to be thread-safe, which is what the CCPLWinSharedResource template does (see Chapter 6 for its usage).

A typical implementation of a call object spawns a thread in response to an incoming Begin call and delegates the processing to the thread. When the thread finishes processing, it needs to notify the call object by calling the ISynchronize::Signal method. My implementation of the call object uses a thread class, CMyThread, to accomplish this. The implementation of its main thread procedure is shown here:

 void CMyThread::Proc()  {   _ASSERT (NULL != m_pCallObject);    // Step 1    // For the simulation, sleep for five seconds    // before computing the sum.    ::Sleep(5 * 1000);    m_pCallObject->m_lSum =      m_pCallObject->m_lVal1 + m_pCallObject->m_lVal2;    // Step 2    // Signal the call object that the sum has been computed    CComPtr<ISynchronize> spSync;    HRESULT hr =      m_pCallObject->QueryInterface(__uuidof(ISynchronize),        (void**) &spSync);    _ASSERT (SUCCEEDED(hr));    spSync->Signal();  } 

When the finished state is signaled, the stub calls Finish_GetSum on the call object to obtain the output parameters. Following is the implementation of the Finish_GetSum method:

 STDMETHODIMP CMyAsyncSumCallObject::Finish_GetSum(   /*[out, retval]*/ long* plSum)  {   *plSum = 0; // initialize    // Ensure that begin has been called    SAFEBOOL::GUARD guard(m_bCallInProgress);    bool& bCallInProgress = guard;    if (false == bCallInProgress) {     // "Finish" called before "Begin"      return RPC_E_CALL_COMPLETE;    }    // wait till the thread signals the completion    CComPtr<ISynchronize> spSync;    HRESULT hr = this->QueryInterface(__uuidof(ISynchronize),      (void**) &spSync);    _ASSERT (SUCCEEDED(hr));    spSync->Wait(0, INFINITE);    spSync = NULL;    *plSum = m_lSum;    bCallInProgress = false;    return S_OK;  } 

Note that Finish_GetSum calls the ISynchronize::Wait method. This will ensure that Finish will not return bogus results if it is called before the spawned thread finishes its processing.

Why can t you just wait for the thread handle to close instead? Well, there is a problem doing so. When the spawned thread calls ISynchronize::Signal, the stub uses the same thread to call Finish_GetSum. Finish_GetSum would then be waiting for the current thread handle to close, resulting in a deadlock.

Canceling a Synchronous Call

Whether or not you use the asynchronous call mechanism in your application, there is one COM+ feature that deserves special attention. Under Windows 2000, it is possible to cancel a synchronous blocking call! Here s how it works.

Obviously, a thread that is waiting for the blocking call to return cannot cancel itself. However, another thread can cancel the call on the calling thread s behalf. First, the thread obtains the ICancelMethodCalls interface by calling a Win32 API, CoGetCancelObject. Then it calls ICancelMethodCalls::Cancel. The implementation is shown below:

 void CMyCancelThread::Proc()  {   CComPtr<ICancelMethodCalls> spCancelMethodCalls;    HRESULT hr = ::CoGetCancelObject(m_dwThreadId,      __uuidof(ICancelMethodCalls),      (void**) &spCancelMethodCalls);    spCancelMethodCalls->Cancel(0);  } 

Note that the ability to cancel synchronous calls is disabled by default. Each thread that requires the ability to cancel a method call that it makes (from some other thread) must enable call cancellation by calling the CoEnableCallCancellation API. The thread can later disable call cancellation by calling the CoDisableCallCancellation API. However, be aware that enabling call cancellation results in serious performance degradation.


< BACK  NEXT >


COM+ Programming. A Practical Guide Using Visual C++ and ATL
COM+ Programming. A Practical Guide Using Visual C++ and ATL
ISBN: 130886742
EAN: N/A
Year: 2000
Pages: 129

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net