Concurrency Management

In our discussion of standard marshaling, we tacitly assumed that a proxy's call to IRpcChannelBuffer::SendReceive actually works and that a remote object is ready and waiting to receive the call and process it. However, there is always the possibility that the RPC takes an intolerably long time (a network is overloaded, for example) or that the server is blocked in some modal process and cannot take the call.

For this reason, COM provides a service called concurrency management, or message filtering, through which an application (a client or a local or remote server) can filter calls or messages coming into it, allowing the application to handle or reject some calls and defer others. This applies to clients that call objects and objects that call back to a client sink through some notification or event interface.

These calls fall into three categories that help us discern why we'd want to handle certain calls in certain ways, as shown on the following page.8

The vast majority of interface calls are synchronous because asynchronous ones increase programming complexity. A synchronous call means that the client process basically waits in a message loop inside IRpcChannelBuffer::SendReceive until the call is complete. From within this message loop, COM will notify a message filter when certain conditions occur, allowing the caller to time-out as well as handle any incoming calls that may occur in the meantime.

The conditions in question occur when a sequence of calls is made in the same thread of execution that eventually calls back into the original client process. Whenever a client makes a call (or an object sends a notification or fires an event), COM considers this a top-level call and assigns a logical thread ID to it, nominally the caller's HTASK. This is just some machine-unique integer and doesn't bear any relation to the multitasking system thread that is executing the caller's code. This COM thread identifies a series of calls between processes, which might involve multiple system threads.

Anyway, this ID travels from process to process as the recipients of this call themselves make other calls to other processes. This, of course, has the possibility of winding up in a call back to the original top-level process, which is currently waiting for the original top-level call to return. This is the exact condition that COM will detect inside this message loop; otherwise, such a situation would result in utter deadlock. Message filtering is the means by which the original caller, or any caller in between, can handle such a call and return the result through the entire chain.

The core of this mechanism is the message filter, a simple object that implements the IMessageFilter interface. (All of these calls return a DWORD and not an HRESULT.)


interface IMessageFilter : IUnknown
{
DWORD HandleInComingCall (DWORD dwCallType, HTASK threadIDCaller
, DWORD dwTickCount, LPINTERFACEINFO pInterfaceInfo);
DWORD RetryRejectedCall(HTASK threadIDCallee, DWORD dwTickCount
, DWORD dwRejectType);
DWORD MessagePending (HTASK threadIDCallee, DWORD dwTickCount
, DWORD dwPendingType);
};

/*
* Return values for HandleInComingCall and RetryRejectedCall,
* the latter two also being the values for dwRejectType in
* RetryRejectedCall.
*/
typedef enum tagSERVERCALL
{
SERVERCALL_ISHANDLED = 0,
SERVERCALL_REJECTED = 1,
SERVERCALL_RETRYLATER = 2
} SERVERCALL;

It is very important to note that HandleInComingCall is the object side of message filtering; RetryRejectedCall and MessagePending are client-side operations. The object-side member allows that task to handle, delay, or reject calls being made from external clients. The other operations allow the calling client to determine when the object delayed or rejected a call and also to handle other calls that might occur while waiting to try a previously rejected call. These differences apply regardless of who or what the caller and callee are.

In order to handle concurrency issues, you must implement your own simple message filter with this interface and register that filter with COM, which then calls it from within its internal message loop. By default, COM always installs its own standard message filter. Your implementation is a customization of COM's standard filtering service. This is an example of a small object, one without a CLSID and without a server or any registry entries, through which you can customize a standard OLE-provided service.

If you do implement your own message filter, you install it with COM using the function CoRegisterMessageFilter, which has the following arguments:

Argument

Description

pIMessageFilter

The pointer to your IMessageFilter implementation

ppIMsgFilterPrev

A pointer to another IMessageFilter pointer variable that receives, as an out-parameter, the pointer to the previously installed message filter


To unregister your message filter, call CoRegisterMessageFilter(NULL, NULL). Whereas registration will call AddRef on the message filter, unregistration will call Release. Registration, however, never involves global object tables, marshaling, or anything else, even though the name of the API function is almost the same as CoRegisterClassObject. A message filter is a simple object that serves only to customize COM's default message filtering.

8 IDL has keywords that allow you to express these call types for members of a custom interface.