It is easier to support apartment threading in single-threaded apartments because COM provides synchronization on a per-call basis. Supporting free-threading is more difficult because the object must implement synchronization; however, response to clients may be better because synchronization can be implemented for smaller sections of code.
A process is a collection of virtual memory space, code, data, and system resources. A thread is code that is to be serially executed within a process. A processor executes threads, not processes, so each application has at least one process, and a process always has at least one thread of execution, known as the primary thread. A process can have multiple threads in addition to the primary thread.
Processes communicate with one another through messages, using Microsoft's Remote Procedure Call (RPC) technology to pass information to one another. There is no difference to the caller between a call coming from a process on a remote machine and a call coming from another process on the same machine.
When a thread begins to execute, it continues until it is killed or until it is interrupted by a thread with higher priority (by a user action or the kernel's thread scheduler). Each thread can run separate sections of code, or multiple threads can execute the same section of code. Threads executing the same block of code maintain separate stacks. Each thread in a process shares that process's global variables and resources.
The thread scheduler determines when and how often to execute a thread, according to a combination of the process's priority class attribute and the thread's base priority. You set a process's priority class attribute by calling the SetPriorityClass function , and you set a thread's base priority with a call to SetThreadPriority.
Multithreaded applications must avoid two threading problems: deadlocks and races. A deadlock occurs when each thread is waiting for the other to do something. The COM call control helps prevent deadlocks in calls between objects. A race condition occurs when one thread finishes before another on which it depends, causing the former to use an uninitialized value because the latter has not yet supplied a valid one. COM supplies some functions specifically designed to help avoid race conditions in out-of-process servers. (See Out-of-Process Server Implementation Helpers.)
The Apartment and the COM Threading Architecture
While COM supports the single-thread-per-process model prevalent before the introduction of multiple threads of execution, writing code to take advantage of multiple threads makes it possible to create more efficient applications than ever before by allowing a thread, while it waits for some time-consuming operation to complete, to allow another thread to be executed.
Note Using multiple threads is not a guarantee of better performance. In fact, because thread factoring is a difficult problem, using multiple threads often causes performance problems. The key is to use multiple threads only if you are very sure of what you are doing.
In general, the simplest way to view the COM threading architecture is to think of all the COM objects in the process as divided into groups called apartments. A COM object lives in exactly one apartment, in the sense that its methods can legally be directly called only by a thread that belongs to that apartment. Any other thread that wants to call the object must go through a proxy.
There are two types of apartments: single-threaded apartments, and multithreaded apartments.
- Single-threaded apartments consist of exactly one thread, so all COM objects that live in a single-threaded apartment can receive method calls only from the one thread that belongs to that apartment. All method calls to a COM object in a single-threaded apartment are synchronized with the windows message queue for the single-threaded apartment's thread. A process with a single thread of execution is simply a special case of this model.
- Multithreaded apartments consist of one or more threads, so all COM objects that live in an multithreaded apartment can receive method calls directly from any of the threads that belong to the multithreaded apartment. Threads in a multithreaded apartment use a model called free-threading. Calls to COM objects in a multithreaded apartment are synchronized by the objects themselves.
Note For a description of communication between single-threaded apartments and multithreaded apartments within the same process, see Single-Threaded and Multithreaded Communication.
A process can have zero or more single-threaded apartments and zero or one multithreaded apartment.
In a process, the main apartment is the first to be initialized. In a single-threaded process, this is the only apartment. Call parameters are marshaled between apartments, and COM handles the synchronization through messaging. If you designate multiple threads in a process to be free-threaded, all free threads reside in a single apartment, parameters are passed directly to any thread in the apartment, and you must handle all synchronization. In a process with both free-threading and apartment threading, all free threads reside in a single apartment and all other apartments are single-threaded apartments. A process that does COM work is a collection of apartments with, at most, one multithreaded apartment but any number of single-threaded apartments.
The threading models in COM provide the mechanism for clients and servers that use different threading architectures to work together. Calls among objects with different threading models in different processes are naturally supported. From the perspective of the calling object, all calls to objects outside a process behave identically, no matter how the object being called is threaded. Likewise, from the perspective of the object being called, arriving calls behave identically, regardless of the threading model of the caller.
Interaction between a client and an out-of-process object is straightforward, even when they use different threading models because the client and object are in different processes. COM, interposed between the client and the server, can provide the code for the threading models to interoperate, using standard marshaling and RPC. For example, if a single-threaded object is called simultaneously by multiple free-threaded clients, the calls will be synchronized by COM by placing corresponding window messages in the server's message queue. The object's apartment will receive one call each time it retrieves and dispatches messages. However, some care must be taken to ensure that in-process servers interact properly with their clients. (See In-Process Server Threading Issues.)
The most important issue in programming with a multithreaded model is to make your code thread-safe so that messages intended for a particular thread go only to that thread and access to threads is protected.
An in-process server does not call CoInitialize, CoInitializeEx, or OleInitialize to mark its threading model. For thread-aware DLL-based or in-process objects, you need to set the threading model in the registry. The default model when you do not specify a threading model is single-thread-per-process. To specify a model, you add theThreadingModel value to the InprocServer32 key in the registry.
DLLs that support instantiation of a class object must implement and export the functions DllGetClassObjectand DllCanUnloadNow. When a client wants an instance of the class the DLL supports, a call toCoGetClassObject (either directly or through a call to CoCreateInstance) calls DllGetClassObject to get a pointer to its class object when the object is implemented in a DLL. DllGetClassObject should therefore be able to give away multiple class objects or a single thread-safe object (essentially just usingInterlockedIncrement/InterlockedDecrement on their internal reference counts).
As its name implies, DllCanUnloadNow is called to determine whether the DLL that implements it is in use, enabling the caller to safely unload it if it is not. Calls to CoFreeUnusedLibraries from any thread always route through the main apartment's thread to call DllCanUnloadNow.
Like other servers, in-process servers can be single-threaded, apartment-threaded, or free-threaded. These servers can be used by any OLE client, regardless of the threading model used by that client.
All combinations of threading model interoperability are allowed between clients and in-process objects. Interaction between a client and an in-process object that use different threading models is exactly like the interaction between clients and out-of-process servers. For an in-process server, when the threading model of the client and in-process server differ, COM must interpose itself between the client and the object.
When an in-process object that supports the single-threaded model is called simultaneously by multiple threads of a client, COM cannot allow the client threads to directly access the object's interface—the object was not designed for such access. Instead, COM must ensure that calls are synchronized and are made only by the client thread that created the object. Therefore, COM creates the object in the client's main apartment and requires all the other client apartments to access the object by using proxies.
When a free-threaded apartment (multithreaded apartment model) in a client creates an apartment-threaded in-process server, COM spins up a single-threaded apartment model "host" thread in the client. This host thread will create the object, and the interface pointer will be marshaled back to the client's free-threaded apartment. Similarly, when a single-threaded apartment in an apartment-model client creates a free-threaded in-process server, COM spins up a free-threaded host thread (multithreaded apartment on which the object will be created and then marshaled back to the client single-threaded apartment).
Note In general, if you design a custom interface on an in-process server, you should also provide the marshaling code for it so that COM can marshal the interface between client apartments.
COM helps protect access to objects provided by a single-threaded DLL by requiring access from the same client apartment in which they were created. In addition, all of the DLL entry points (like DllGetClassObject andDllCanUnloadNow) and global data should always be accessed by the same apartment. COM creates such objects in the main apartment of the client, giving the main apartment direct access to the object's pointers. Calls from the other apartments use interthread marshaling to go from the proxy to the stub in the main apartment and then to the object. This allows COM to synchronize calls to the object. Interthread calls are slow, so it is recommended that these servers be rewritten to support multiple apartments.
Like a single-threaded in-process server, an object provided by an apartment model DLL must be accessed by the same client apartment from which it was created. However, objects provided by this server can be created in multiple apartments of the client, so the server must implement its entry points (like DllGetClassObject andDllCanUnloadNow) for multithreaded use. For example, if two apartments of a client try to create two instances of the in-process object simultaneously, DllGetClassObject can be called simultaneously by both apartments.DllCanUnloadNow must be written so that the DLL does not unload while code is still executing in the DLL.
If the DLL provides only one instance of the class factory to create all the objects, the class factory implementation must also be designed for multithreaded use, because it will be accessed by multiple client apartments. If the DLL creates a new instance of the class factory each time DllGetClassObject is called, the class factory need not be thread-safe.
Objects created by the class factory need not be thread-safe. Once created by a thread, the object is always accessed through that thread and all calls to the object are synchronized by COM. The apartment model apartment of a client that creates this object will get a direct pointer to the object. Client apartments that are different from the apartment in which the object was created must access the object through proxies. These proxies are created when the client marshals the interface between its apartments.
When an in-process DLL ThreadingModel value is set to "Both", an object provided by this DLL can be created and used directly (without a proxy) in single-threaded or multithreaded client apartments. However, it can be used directly only within the apartment in which it was created. To give the object to any other apartment, the object must be marshaled. The DLL object must implement its own synchronization and can be accessed by multiple client apartments at the same time.
To speed performance for free-threaded access to in-process DLL objects, COM provides theCoCreateFreeThreadedMarshaler function. This function creates a free-threaded marshaling object that can be aggregated with an in-process server object. When a client apartment in the same process needs access to an object in another apartment, aggregating the free-threaded marshaler provides the client with a direct pointer to the server object, rather than to a proxy, when the client marshals the object's interface to a different apartment. The client does not need to do any synchronization. This works only within the same process—standard marshaling is used for a reference to the object that is sent to another process.
An object provided by an in-process DLL that supports only free threading is a free-threaded object. It implements its own synchronization and can be accessed by multiple client threads at the same time. This server does not marshal interfaces between threads, so this server can be created and used directly (without a proxy) only by multithreaded apartments in a client. Single-threaded apartments that create it will access it through a proxy.
Using single-threaded apartments (the apartment model process) offers a message-based paradigm for dealing with multiple objects running concurrently. It enables you to write more efficient code by allowing a thread, while it waits for some time-consuming operation to complete, to allow another thread to be executed.
Each thread in a process that is initialized as an apartment model process, and that retrieves and dispatches window messages, is a single-threaded apartment thread. Each thread lives within its own apartment. Within an apartment, interface pointers can be passed without marshaling, and therefore, all objects in one single-threaded apartment thread communicate directly.
A logical grouping of related objects that all execute on the same thread, and therefore must have synchronous execution, could live on the same single-threaded apartment thread. However, an apartment model object cannot reside on more than one thread. Calls to objects in other processes must be made within the context of the owning process, so distributed COM switches threads for you automatically when you call on a proxy.
The interprocess and interthread models are similar. When it is necessary to pass an interface pointer to an object in another apartment (on another thread) within the same process, you use the same marshaling model that objects in different processes use to pass pointers across process boundaries. By getting a pointer to the standard marshaling object, you can marshal interface pointers across thread boundaries (between apartments) in the same way you do between processes. (Interface pointers must be marshaled when passed between apartments.)
Rules for single-threaded apartments are simple, but it is important to follow them carefully:
- Every object should live on only one thread (within a single-threaded apartment).
- Initialize the COM library for each thread.
- Marshal all pointers to objects when passing them between apartments.
- Each single-threaded apartment must have a message loop to handle calls from other processes and apartments within the same process. Single-threaded apartments without objects (client only) also need a message loop to dispatch the broadcast messages that some applications use.
- DLL-based or in-process objects do not call the COM initialization functions; instead, they register their threading model with the ThreadingModel named-value under the InprocServer32 key in the registry. Apartment-aware objects must also write DLL entry points carefully. There are special considerations that apply to threading in-process servers. For more information, see In-Process Server Threading Issues.
While multiple objects can live on a single thread, no apartment model object can live on more than one thread.
Each thread of a client process or out-of-process server must call CoInitialize, or call CoInitializeEx and specify COINIT_APARTMENTTHREADED for the dwCoInit parameter. The main apartment is the thread that calls CoInitializeEx first. For information on in-process servers, see In-Process Server Threading Issues.
All calls to an object must be made on its thread (within its apartment). It is forbidden to call an object directly from another thread; using objects in this free-threaded manner could cause problems for applications. The implication of this rule is that all pointers to objects must be marshaled when passed between apartments. COM provides the following two functions for this purpose:
- CoMarshalInterThreadInterfaceInStream marshals an interface into a stream object that is returned to the caller.
- CoGetInterfaceAndReleaseStream unmarshals an interface pointer from a stream object and releases it.
These functions wrap calls to CoMarshalInterface and CoUnmarshalInterface functions, which require the use of the MSHCTX_INPROC flag.
In general, the marshaling is accomplished automatically by COM. For example, when passing an interface pointer as a parameter in a method call on a proxy to an object in another apartment, or when callingCoCreateInstance, COM does the marshaling automatically. However, in some special cases, where the application writer is passing interface pointers between apartments without using the normal COM mechanisms, the writer must handle the marshaling manually.
If one apartment (Apartment 1) in a process has an interface pointer and another apartment (Apartment 2) requires its use, Apartment 1 must call CoMarshalInterThreadInterfaceInStream to marshal the interface. The stream that is created by this function is thread-safe and must be stored in a variable that is accessible by Apartment 2. Apartment 2 must pass this stream to CoGetInterfaceAndReleaseStream to unmarshal the interface and will get back a pointer to a proxy through which it can access the interface. The main apartment must remain alive until the client has completed all COM work (because some in-process objects are loaded in the main apartment, as described in In-Process Server Threading Issues). After one object has been passed between threads in this manner, it is very easy to pass interface pointers as parameters. That way, distributed COM does the marshaling and thread switching for the application.
To handle calls from other processes and apartments within the same process, each single-threaded apartment must have a message loop. This means that the thread's work function must have a GetMessage/DispatchMessage loop. If other synchronization primitives are being used to communicate between threads, the MsgWaitForMultipleObjects function can be used to wait both for messages and for thread synchronization events. The documentation for this function has an example of this sort of combination loop.
COM creates a hidden window using the Windows class "OleMainThreadWndClass" in each single-threaded apartment. A call to an object is received as a window message to this hidden window. When the object's apartment retrieves and dispatches the message, the hidden window will receive it. The window procedure will then call the corresponding interface method of the object.
When multiple clients call an object, the calls are queued in the message queue and the object will receive a call each time its apartment retrieves and dispatches messages. Because the calls are synchronized by COM and the calls are always delivered by the thread that belongs to the object's apartment, the object's interface implementations need not provide synchronization. Single-threaded apartments can implement IMessageFilterto permit them to cancel calls or receive window messages when necessary.
The object can be reentered if one of its interface method implementations retrieves and dispatches messages or makes an ORPC call to another thread, thereby causing another call to be delivered to the object (by the same apartment). OLE does not prevent reentrancy on the same thread, but it can help provide thread safety. This is identical to the way in which a window procedure can be reentered if it retrieves and dispatches messages while processing a message. However, calling an out-of-process single-threaded apartment server that calls another single-threaded apartment server will allow the first server to be reentered.
COM provides a way for any apartment in a process to get access to an interface implemented on an object in any other apartment in the process. This is done through the IGlobalInterfaceTable interface. This interface has three methods, which allow you to do the following:
- Register an interface as a global (processwide) interface.
- Get a pointer to that interface from any other apartment through a cookie.
- Revoke the global registration of an interface.
The IGlobalInterfaceTable interface is an efficient way for a process to store an interface pointer in a memory location that can be accessed from multiple apartments within the process, such as process-wide variables andagile objects (free-threaded, marshaled objects) containing interface pointers to other objects.
An agile object is unaware of the underlying COM infrastructure in which it runs; in other words, what apartment, context, and thread it is executing on. The object may be holding on to interfaces that are specific to an apartment or context. For this reason, calling these interfaces from wherever the agile component is executing might not always work properly. The global interface table avoids this problem by guaranteeing that a valid proxy (or direct pointer) to the object is used, based on where the agile object is executing.
Note The global interface table is not portable across process or machine boundaries, so it cannot be used in place of the normal parameter-passing mechanism.
If you are unmarshaling an interface pointer multiple times between apartments in a process, you might use theIGlobalInterfaceTable interface. With other techniques, you would have to remarshal each time.
Note If the interface pointer is unmarshaled only once, you may want to use theCoMarshalInterThreadInterfaceInStream function. It can also be used to pass an interface pointer from one thread to another thread in the same process.
The IGlobalInterfaceTable interface also makes another previously difficult problem simpler for the programmer. This problem occurs when the following conditions apply:
- An in-process agile object aggregates the free-threaded marshaler.
- This same agile object also holds (as member variables) interface pointers to other objects that are not agile and do not aggregate the free-threaded marshaler.
In this situation, if the outer object gets marshaled to another apartment and the application calls on it, and the object tries to call on any of its member variable interface pointers that are not free-threaded or are proxies to objects in other apartments, it might get incorrect results or the error RPC_E_WRONG_THREAD. This error occurs because the inner interface is designed to be callable only from the apartment in which it was first stored in the member variable.
To solve this problem, the outer object aggregating the free-threaded marshaler should callIGlobalInterfaceTable::RegisterInterfaceInGlobal on the inner interface and store the resulting cookie in its member variable, instead of storing the actual interface pointer. When the outer object wants to call on an inner object's interface pointer, it should call IGlobalInterfaceTable::GetInterfaceFromGlobal, use the returned interface pointer, and then release it. When the outer object goes away, it should callIGlobalInterfaceTable::RevokeInterfaceFromGlobal to remove the interface from the global interface table
Use the following call to create the global interface table object and get a pointer to IGlobalInterfaceTable:
HRESULT hr;
hr = CoCreateInstance(CLSID_StdGlobalInterfaceTable,
NULL,
CLSCTX_INPROC_SERVER,
IID_IGlobalInterfaceTable,
(void **)&gpGIT);
if (hr != S_OK) {
exit(0); // Handle errors here.
}
Note When creating the global interface table object using the preceding call, it is necessary to link to the library uuid.lib. This will resolve the external symbols CLSID_StdGlobalInterfaceTable and IID_IGlobalInterfaceTable.
There is a single instance of the global interface table per process, so all calls to this function in a process return the same instance.
After the call to the CoCreateInstance function, register the interface from the apartment in which it resides with a call to the RegisterInterfaceInGlobal method. This method supplies a cookie that identifies the interface and its location. An apartment seeking a pointer to this interface then calls the GetInterfaceFromGlobalmethod with this cookie, and the implementation then supplies an interface pointer to the calling apartment. To revoke the interface's global registration, any apartment can call the RevokeInterfaceFromGlobal method.
A simple example of using IGlobalInterfaceTable would be when you want to pass an interface pointer on an object in a single-threaded apartment (STA) to a worker thread in another apartment. Rather than having to marshal it into a stream and pass the stream to the worker thread as a thread parameter,IGlobalInterfaceTable allows you simply to pass a cookie.
When you register the interface in the global interface table, you get a cookie that you can use instead of passing the actual pointer (whenever you need to pass the pointer), either to a nonmethod parameter that is going to another apartment (as a parameter to ThreadProc through CreateThread) or to in-process memory accessible outside your apartment.
Care is required because using global interfaces places the extra burden on the programmer of managing problems such as race conditions and mutual exclusion, which are associated with accessing global state from multiple threads simultaneously.
COM provides a standard implementation of the IGlobalInterfaceTable interface. It is highly recommended that you use this standard implementation because it provides complete thread-safe functionality.
In a multithreaded apartment model, all the threads in the process that have been initialized as free-threaded reside in a single apartment. Therefore, there is no need to marshal between threads. The threads need not retrieve and dispatch messages because COM does not use window messages in this model.
Calls to methods of objects in the multithreaded apartment can be run on any thread in the apartment. There is no serialization of calls; many calls may occur to the same method or to the same object simultaneously. Objects created in the multithreaded apartment must be able to handle calls on their methods from other threads at any time.
Because calls to objects are not serialized in any way, multithreaded object concurrency offers the highest performance and takes the best advantage of multiprocessor hardware for cross-thread, cross-process, and cross-machine calling. This means, however, that the code for objects must provide synchronization in their interface implementations, typically through the use of synchronization primitives such as event objects, critical sections, mutexes, or semaphores, which are described later in this section. In addition, because the object doesn't control the lifetime of the threads that are accessing it, no thread-specific state may be stored in the object (in thread local storage).
Following are some important considerations regarding synchronization for multithreaded apartments:
- COM provides call synchronization for single-threaded apartments only.
- Multithreaded apartments do not receive calls while making calls (on the same thread).
- Multithreaded apartments cannot make input-synchronized calls.
- Asynchronous calls are converted to synchronous calls in multithreaded apartments.
- The message filter is not called for any thread in a multithreaded apartment.
To initialize a thread as free-threaded, call CoInitializeEx, specifying COINIT_MULTITHREADED. For information on in-process server threading, see In-Process Server Threading Issues.
Multiple clients can simultaneously call, from different threads, an object that supports free-threading. In free-threaded out-of-process servers, COM, through the RPC subsystem, creates a pool of threads in the server process and a client call (or multiple client calls) can be delivered by any of these threads at any time. An out-of-process server must also implement synchronization in its class factory. Free-threaded, in-process objects can receive direct calls from multiple threads of the client.
The client can do COM work in multiple threads. All threads belong to the same multithreaded apartment. Interface pointers are passed directly from thread to thread within a multithreaded apartment, so interface pointers are not marshaled between its threads. Message filters (implementations of IMessageFilter) are not used in multithreaded apartments. The client thread will suspend when it makes a COM call to out-of-apartment objects and will resume when the call returns. Calls between processes are still handled by RPC.
Threads initialized with the free-threaded model must implement their own synchronization. As mentioned earlier in this section, Windows enables this implementation through the following synchronization primitives:
- Event objects provide a way of signaling one or more threads that an event has occurred. Any thread within a process can create an event object. A handle to the event is returned by the event-creating function, CreateEvent. Once an event object has been created, threads with a handle to the object can wait on it before continuing execution.
- Critical sections are used for a section of code that requires exclusive access to some set of shared data before it can be executed and that is used only by the threads within a single process. A critical section is like a turnstile through which only one thread at a time may pass, working as follows:
- To ensure that no more than one thread at a time accesses shared data, a process's primary thread allocates a global CRITICAL_SECTION data structure and initializes its members. A thread entering a critical section calls the EnterCriticalSection function and modifies the data structure's members.
- A thread attempting to enter a critical section calls EnterCriticalSection which checks to see whether the CRITICAL_SECTION data structure has been modified. If so, another thread is currently in the critical section and the subsequent thread is put to sleep. A thread leaving a critical section calls LeaveCriticalSection, which resets the data structure. When a thread leaves a critical section, the system wakes one of the sleeping threads, which then enters the critical section.
- Mutexes performs the same function as a critical section, except that the mutex is accessible to threads running in different processes. Owning a mutex object is like having the floor in a debate. A process creates a mutex object by calling the CreateMutex function, which returns a handle. The first thread requesting a mutex object obtains ownership of it. When the thread has finished with the mutex, ownership passes to other threads on a first-come, first-served basis.
- Semaphores are used to maintain a reference count on some available resource. A thread creates a semaphore for a resource by calling the CreateSemaphore function and passing a pointer to the resource, an initial resource count, and the maximum resource count. This function returns a handle. A thread requesting a resource passes its semaphore handle in a call to the WaitForSingleObjectfunction. The semaphore object polls the resource to determine whether it is available. If so, the semaphore decrements the resource count and wakes the waiting thread. If the count is zero, the thread remains asleep until another thread releases a resource, causing the semaphore to increment the count to one.
提及COM的线程模式,实际上指的是两个方面,一个是客户程序的线程模式,一个是组件所支持的线程模式。客户程序的线程模式只有两种,单线程公寓(STA)和多线程公寓(MTA)。组件所支持的线程模式有四种:Single(单线程)、Apartment(STA)、Free(MTA)、Both(STA+MTA)。
1、公寓只是个逻辑上的概念。一个STA只能包含一个线程,一个MTA可以包含多个线程。一个进程可以包含多个STA,但只能有一个MTA。MTA中各线程可以并行的调用本公寓内实例化的组件,而不需要进行调度。跨公寓调用组件实例必须要进行调度。(除非使用了*线程调度器)
2、客户程序的线程是在调用CoInitializeEx()时决定客户线程的类型的。如果以参数COINIT_APARTMENTTHREADED调用,则会创建一个STA公寓,客户线程包含在这个公寓里。如果以参数COINIT_MULTITHREADED调用,则创建一个MTA公寓,把线程加入到这个MTA中;如果进程内已经有了一个MTA,则不创建新的MTA,只把线程加入到已有的MTA。注意每个线程都必须调用CoInitializeEx()才能使用COM组件。
3、线程最重要的是同步问题。STA是通过窗口消息队列来解决这个问题的。当客户线程以COINIT_APARTMENTTHREADED调用CoInitializeEx()时,将为会该STA创建一个具有OleMainThreadWndClass窗口类的隐含窗口。所有对在这个公寓中建立的COM对象方法的调用都将都放到这个隐含窗口的消息队列中。所以每一个与STA相关联的线程必须用GetMessage、DispatchMessage或类似方法来分派窗口消息。MTA内各线程可并行调用同一个组件对象的实例,从而不保证安全性,所以实现同步访问的责任就落在了组件身上。注意,STA的同步是公寓级的,就是说对公寓内不同组件的访问都要放到同一个消息队列中,对一个实例的方法调用会影响对其他实例的调用,所以并发程度很低。
4、在不同公寓间传递接口指针必须要经过调度。这主要还是为了同步对组件的调用。通过CoMarshalInterThreadInterfaceInStream和CoGetInterfaceAndReleaseStream实现。很简单。
5、Single型组件很特殊,它只能在一个单一的线程中执行。首先要说明的是一个进程中第一个以COINIT_APARTMENTTHREADED调用CoInitializeEx()的线程被称作是主STA。每次用CoCreateInstance()创建的Single型组件实际上都是创建在了这个主STA中,而不管是谁调用了CoCreateInstance()这个函数。所有对这个Single组件方法的调用都必须要通过这个主STA。
6、若STA创建STA型组件,是直接创建,直接调用。若STA创建MTA型组件,系统为组件创建一个MTA,STA通过代理访问组件。若STA创建Both型组件,是直接创建,直接调用。若MTA创建STA型组件,系统为组件创建一个STA,MTA通过代理访问组件。若MTA创建MTA型组件,是直接创建,直接调用。若MTA创建Both型组件,是直接创建,直接调用。可见如果客户程序和组件都支持同样的线程模式,那么COM就允许客户程序直接调用对象,这样将产生最佳性能。
7、Both型组件已经很好了,无论是STA还是MTA都可以直接创建调用它。但跨公寓的调用仍然要经过代理。为了更进一步以获得最佳性能,可以使用*线程调度器(FTM)。注意其它类型的组件也可以使用FTM,只是由Both使用FTM可获得是最佳效果。FTM实现了接口IMarshal,当调度那两个调度接口指针的函数时,这两个函数(见5)内部调用IMarshal内的相关函数,并判断如果调度发生在一个进程内的公寓之间则直接返回接口指针;如果调度发生在进程之间或者远程计算机间,则调用标准的调度器,并返回指向代理对象的指针。所以可见使用FTM,即使是公寓之间也不用调度接口指针了!!
8、FTM虽然好,但使用FTM的组件必须遵守某些限制:使用FTM的对象不能直接拥有没有实现FTM的对象的接口指针;使用FTM的对象不能拥有其他公寓对象代理的引用。
9、全局接口表(GIT)。作用范围是进程内。可以把接口指针存进表中,然后在别的公寓内把其取出,GIT自动执行公寓间的调度,所以很方便。GIT是通过IGlobalInterfaceTable访问的。通过创建CLSID为CLSID_StdGlobalInterfaceTable的对象可调用它。
1)COM对象为STA,Client线程初始化为COINIT_APARTMENTTHREADED
COM Runtime将建立STA COM对象,Client的线程进入这个STA并直接得到COM对象指针.
2)COM对象为STA,Client线程初始化为COINIT_MULTITHREADED
如果这个COM对象没有被建立过,COM Runtime将为这个COM对象建立一个新的线程并在这个新线程中建立STA COM对象, Client线程进入的是MTA,得到的将是新的线程中这个COM对象的被marshal的指针.如果这个COM对象被建立过,COM Client线程得到的将是已经建立的STA线程中COM对象的被marshal的指针.
3)COM对象为MTA,Client线程初始化为COINIT_APARTMENTTHREADED
如果这个COM对象没有被建立过,COM Runtime将为这个COM对象建立一个新的线程并在这个新线程中建立MTA COM对象, COM Client线程进入STA,得到的将是新的线程中这个COM对象的被marshal的指针.如果这个COM对象被建立过,Client线程得到的将是已经建立的MTA线程中COM对象的被marshal的指针.
4)COM对象为MTA,COM Client为COINIT_MULTITHREADED
如果这个COM对象没有被建立过,COM Runtime将建立MTA COM对象.COM Client线程进入MTA并直接得到COM对象的指针.如果这个COM对象被初始化过, Client线程进入已经建立的MTA并直接得到COM对象的指针.
5)COM对象为Any,COM Client为COINIT_APARTMENTTHREADED
如果这个COM对象没有被初始化过,COM Runtime将建立STA COM对象,Client线程进入STA,直接得到COM对象指针.
6)COM对象为Any,COM Client为COINIT_MULTITHREADED
如果这个COM对象没有被初始化过,COM Runtime将建立MTA COM对象,Client线程进入这个MTA并直接得到COM对象的指针.如果这个COM对象被初始化过,Client线程进入已经建立的MTA并直接得到COM对象的指针.
4. 关于marshal
在COM中marshal分为三种: 进程内的marshal, 同一计算机中进程间的marshal, 以及不同计算机间的marshal. 进程内和进程间的marshal是通过Local RPC完成的,计算机间的marshal通过DCE RPC来完成. 进程间和计算机间的marshal是必须的,进程内marshal是在不同Apartment之间进行方法调用和传递对象Interface时发生.在同一Apartment内的调用用不着marshal. 举个例子来说, 一个处于MTA中的Client线程,想要调用一个处于STA中的对象时,
COM Runtime会走进来, 对这个调用进行marshal.为什么要marshal? 因为MTA本身说明了现在是一个多线程的环境, 而STA中的对象不是Thread-safe的,那么对这个不是Thread-safe的对象的调用必须要序列化(排队).COM Runtime为了保证不是Thread-safe的对象的调用序列化, 必须要截获对该对象的调用, 然后进行排队. marshal就是起这个作用. 实际上,COM Runtime会截获MTA的线程对STA对象的调用(通过Proxy),将这个调用通过消息传递方式传递给STA对象的stub,
在完成调用后由stub将结果传回Proxy. 对于对象的Interface, 也是同样的道理.
对于COM+的Thread-Neutral Apartment比较特殊, 它是一直需要marshal的, 一个方面是因为它处于不同的进程中.另外一个重要原因是, COM+提供了一系列新的功能, 如Object Pooling, Object Construct String等. COM+必须要截获Client对COM对象的调用才能完成将COM对象从缓冲池中取出以及放回缓冲池等的操作.
那么marshal是自动还是手工完成的呢? 方法调用是自动完成的.对象的Interface的传递,一般情况下, 是自动完成的,比如你通过调用CoCreateInstanceEx,CoGetClassObject等得到的对象Interface,以及通过方法调用传递的对象Interface.但是有些情况下必须手工marshal. 还是形象写,举个例子: 比如我有一个COM Server, 它监视一个工控装置的信号, 如果信号有异常,它要通知客户端,让客户端进行报警动作.为了实现这个功能, 客户端和我的COM
Server通过IConnectionPoint完成事件触发机制.为了提高性能, COM Server的主线程接受客户端通过IConnectionPoint的Advise传来的Interface指针, 并将之放到一个Interface指针表里, 主线程运行在STA中. 另外建立了一个运行于MTA中的线程专门用来监视信号,如果信号异常,它将调用Interface指针表里所有Interface的方法来通知客户端. 现在如果理解COM的机制的人看到我这个实现方法就知道这里面需要对Interface指针进行手工marshal.
为什么, 客户端通过IConnectionPoint传给我的COM Server主线程的Interface指针是自动进行了marshal, 但是, 由于我的COM Server的专门用来监视信号的线程运行在和主线程不同的Apartment之中, 对这个线程来说, 这些Interface指针是没有经过marshal的, 在调用是就会出现RPC_E_WRONG_THREAD错误. 要解决这个问题,有两个办法,
1) 让我的主线程也运行在MTA中.这种方法简单.
2) 手工marshal.
在主线程中得到客户端的Interface指针后, 调用CoMarshalInterThreadInterfaceInStream, 得到一个IStream的指针,让后将它放到IStream的指针表里, 监视信号的线程要通知客户端时,从IStream的指针表取得IStream指针, 然后调用CoGetInterfaceAndReleaseStream得到marshal后的客户端Interface指针. 这种方法有个缺点, 就是一旦调用CoGetInterfaceAndReleaseStream后这个IStream指针就被释放掉了,下一次就取不到了.
更好的解决方法是采用GIT(global interface table), 主线程将它得到的Interface指针放到GIT, 监视信号的线程从GIT中取到的Interface指针是正确marshal了的. GIT是一个COM对象, 有三个方法提供Interface指针的存取,使用也很简单,这儿就不多说了,具体请参照帮助.
总结精华:
提及COM的线程模式,实际上指的是两个方面,一个是客户程序的线程模式,一个是组件所支持的线程模式。客户程序的线程模式只有两种,单线程公寓(STA)和多线程公寓(MTA)。组件所支持的线程模式有四种:Single(单线程)、Apartment(STA)、Free(MTA)、Both(STA+MTA)。
2。 公寓只是个逻辑上的概念。一个STA只能包含一个线程,一个MTA可以包含多个线程。一个进程可以包含多个STA,但只能有一个MTA。MTA中各线程可以并行的调用本公寓内实例化的组件,而不需要进行调度。跨公寓调用组件实例必须要进行调度。(除非使用了*线程调度器)。
3。 客户程序的线程是在调用CoInitializeEx()时决定客户线程的类型的。如果以参数 COINIT_APARTMENTTHREADED调用,则会创建一个STA公寓,客户线程包含在这个公寓里。如果以参数COINIT_MULTITHREADED调用,则创建一个MTA公寓,把线程加入到这个MTA中;如果进程内已经有了一个MTA,则不创建新的MTA,只把线程加入到已有的MTA。注意每个线程都必须调用CoInitializeEx()才能使用COM组件。
4。 线程最重要的是同步问题。STA是通过窗口消息队列来解决这个问题的。当客户线程以 COINIT_APARTMENTTHREADED调用CoInitializeEx()时,将为会该STA创建一个具有 OleMainThreadWndClass窗口类的隐含窗口。所有对在这个公寓中建立的COM对象方法的调用都将都放到这个隐含窗口的消息队列中。所以每一个与STA相关联的线程必须用 GetMessage、DispatchMessage或类似方法来分派窗口消息。MTA内各线程可并行调用同一个组件对象的实例,从而不保证安全性,所以实现同步访问的责任就落在了组件身上。注意,STA的同步是公寓级的,就是说对公寓内不同组件的访问都要放到同一个消息队列中,对一个实例的方法调用会影响对其他实例的调用,所以并发程度很低。
5。 在不同公寓间传递接口指针必须要经过调度。这主要还是为了同步对组件的调用。通过CoMarshalInterThreadInterfaceInStream和CoGetInterfaceAndReleaseStream实现。很简单。
6。 Single型组件很特殊,它只能在一个单一的线程中执行。首先要说明的是一个进程中第一个以COINIT_APARTMENTTHREADED调用CoInitializeEx()的线程被称作是主STA。每次用CoCreateInstance()创建的Single型组件实际上都是创建在了这个主STA中,而不管是谁调用了CoCreateInstance()这个函数。所有对这个Single组件方法的调用都必须要通过这个主STA。
7。 若STA创建STA型组件,是直接创建,直接调用。
若STA创建MTA型组件,系统为组件创建一个MTA,STA通过代理访问组件。
若STA创建Both型组件,是直接创建,直接调用。
若MTA创建STA型组件,系统为组件创建一个STA,MTA通过代理访问组件。
若MTA创建MTA型组件,是直接创建,直接调用。
若MTA创建Both型组件,是直接创建,直接调用。
可见如果客户程序和组件都支持同样的线程模式,那么COM就允许客户程序直接调用对象,这样将产生最佳性能。
8。 Both型组件已经很好了,无论是STA还是MTA都可以直接创建调用它。但跨公寓的调用仍然要经过代理。为了更进一步以获得最佳性能,可以使用*线程调度器(FTM)。注意其它类型的组件也可以使用FTM,只是由Both使用FTM可获得是最佳效果。FTM实现了接口IMarshal,当调度那两个调度接口指针的函数时,这两个函数(见5)内部调用IMarshal内的相关函数,并判断如果调度发生在一个进程内的公寓之间则直接返回接口指针;
如果调度发生在进程之间或者远程计算机间,则调用标准的调度器,并返回指向代理对象的指针。所以可见使用FTM,即使是公寓之间也不用调度接口指针了!!
9。 FTM虽然好,但使用FTM的组件必须遵守某些限制:使用FTM的对象不能直接拥有没有实现FTM的对象的接口指针;使用FTM的对象不能拥有其他公寓对象代理的引用。
10。 全局接口表(GIT)。作用范围是进程内。可以把接口指针存进表中,然后在别的公寓内把其取出,GIT自动执行公寓间的调度,所以很方便。GIT是通过IGlobalInterfaceTable访问的。通过创建CLSID为CLSID_StdGlobalInterfaceTable的对象可调用它。