Here we will look in some detail at how calls are dispatched in
STAs and in the MTA.
| Threads as clients and servers : incoming and
outgoing calls |
An apartment can be thought of as an execution environment.
Threads in the MTA or a thread in an STA can be performing one of
two operations with regard to objects:
- Making an outbound call, i.e., calling an object within or
outside of the apartment, as the client of an object.
- Receiving an inbound call, i.e., servicing a call from a
thread outside of the apartment, as the server for an object.
If the apartment model is to be of any use, a thread making an
outbound call must not be able to block an incoming call. In
particular, callback scenarios, such as the one outlined in the
following, must work when dispatching calls across apartments.
void ConnectAndMakeCall(IForward
*pForward) { // instantiate an object in this
thread IBackward *pBack = new
CoBacward; // make blocking call to object in
another thread
pFoward->DoSomethingAndNotify(pBack); }
HRESULT
CoForward::DoSomethingAndNotify(IBackward *pBack)
{ long n; // call back
into pBack
pBack->GetNextValue(&n); }
Causality
When a thread that is not currently executing an incoming method
makes an outbound method call out of apartment, it is considered a
top-level call. That is, it establishes a new call-chain.
When a thread makes an outbound method call out of apartment while
servicing an incoming method, it is considered a nested
call. It is part of an existing call-chain. Top-level calls allocate
a new causality ID (CID), which is a GUID that gets propagated from
object to object as the call-chain extends. This is sometime called
the logical thread ID. The CID is part of the ORPC packet
header that describes every cross-apartment method call. Nested
calls execute under the CID of the received packet.
The system uses this mechanism in an STA message filter (see
later) to determine whether an incoming call to an object in an STA
is a top-level call or a nested call. For incoming calls into the
MTA, the CID is not really used by the system (or easily available
to us!).
Let us look first at how calls are dispatched in the MTA and in
an STA.
| Call dispatching in the MTA
|
All COM executables will have an RPC thread cache for servicing
incoming cross-apartment calls. Threads in the RPC thread cache run
in the MTA. When such a thread is needed to dispatch a call to an
object in the MTA, it simply calls the object directly. No attempt
is made to serialize incoming calls in the MTA. Any free thread in
the RPC cache can be used to dispatch calls and so MTA objects do
not have thread affinity.
To see an animation that depicts how calls are dispatched in the
MTA, click
here.
If the client, on the left of the animation, is also a server, it
will have an RPC thread cache. So while one MTA thread is blocked on
an outgoing call, another thread in the RPC thread cache can service
an incoming call; not a problem.
| Call dispatching in an STA
|
The notion of a COM apartment only exists because there are
certain thread-unaware object implementations that must not be
compromised by concurrent multi-threaded access. Bear in mind that
some library code used by object implementations may not be
thread-safe and, worse still, some libraries abuse thread local
storage and have thread affinity. Access to these kinds of objects
must be serialized through a single thread; the same thread every
time. In order to achieve this we have STAs.
Each thread that calls CoInitialize() or
CoInitializeEx(COINIT_APARTMENTTHREADED) is its own single threaded
apartment. All method calls bound for an STA object are serviced and
dispatched by the single thread of the STA that 'owns' the object.
Serialized access to objects in an STA is achieved as follows. A
call on a proxy to an STA object results in a Windows message being
posted to the thread message queue of the STA owning the object.
This message represents the ORPC request. At a later date, the STA
thread will pick up the message and dispatch it to the correct stub.
The stub unmarshalls then calls the object directly.
So while an incoming method call into an STA is being dispatched,
if the object doesn't yield to the message queue
(GetMessage()/PeekMessage()/MessageBox() etc.), no other messages
can be processed from the queue until the method call to the object
has completed. This means during this time no other incoming method
calls into the apartment will be serviced and neither will any user
interface related messages. Essentially, an apartment-wide lock is
acquired on each incoming method call to an object in the apartment
so that the objects within the same STA are serialized with regard
to each other and to the UI of that thread.
This does mean, of course, each STA thread must have a responsive
GetMessage()/DispatchMessage() pump to operate correctly.
It turns out that the first STA thread in a process registers a
window class with a channel-provided window procedure. Each STA
creates an invisible window of this class to receive incoming ORPC
request packets as a private window message. The window procedure
calls the stub to decode the packet and call the object.
To see an animation that depicts how calls are dispatched between
remote STAs, click
here. Notice that the STA thread making the call out needs to
stay responsive during the call, else there is no way that it can
service incoming calls to objects in this STA (especially
callbacks). Notice that the channel in its send/receive (that is
running on the calling STA thread) gets one of the RPC threads in
the MTA to do the blocking send/receive and goes into a message pump
itself. This allows it to service other top-level calls or callbacks
and certain user interface messages. The problem here is that we may
get STA objects called re-entrantly when we are not expecting it and
that may seriously compromise them. More on this later.
To see an animation that depicts how callbacks are serviced in a
remote STA, click
here. Notice how the STA thread making the initial call-out can
still stay responsive to the callback.
Notice also how many threads are used in the remote case. Using
four threads per method call is not ideal. In the local case the
number of threads can be reduced. When communicating on the same
machine there is a special RPC transport that uses window messages
directly. A special blocking hook function is used to implement the
client modal loop in the channel's send/receive to achieve COM
callback semantics.
To see an animation that depicts how calls are dispatched between
local STAs, click
here.
Now we are going to do a lab that illustrates call
dispatching.
This is the point at which you could stop working through
the tutorial. You have now covered the fundamentals of COM.

|