A Simple Example To Explain COM STA, MTA, and Auto-threaded Modules

Environment: VC6 SP5, Visual C++ .Net, Windows 2000

Introduction

This article attempts to illustrate the functions of the COM Single Thread Apartment (STA), Multithread Apartment (MTA) and ATL Auto-thread Module with a testing client/server application. The application includes a console client application, one regular ATL COM Local Server, and one ATL Auto-Threaded server. This article assumes programmers have no intention to implement the concurrency control codes of a COM object.

In the first part of this article, we will review the functionality of the STA and MTA apartment. The second part will introduce the ATL Auto-threaded Server Module. This module requires only a few lines of code on a regular local server, but can improve application performance significantly if multiple client instances access non-singleton COM objects simultaneously.

There are quite a number of changes between VC6 and VC7 (VC++.Net) for the related part this article. Hence, the demo projects are given in both VC6 and VC7 but the source codes referenced here are from VC6.

I have gained a lot from this excellent Web site; I hope this contribution will help COM programmers intensify their COM apartment conception and to use Auto-threaded server modules whenever applicable.

Preparation: Implement a Simple Testing Local Server Application

First, let’s generate a COM local server module called LocalServerDefault using VC6.0 IDE by the following route:

VC6->File->New->Projects->ATL COM AppWizard
   ->Project Name: [LocalServerDefault]
   ->Server Type: Executable (EXE)->Finish

Then, generate a COM object with a single interface called ITestServerDefault:

New ATL Object->Simple Object
              ->Names::Short Name:[TestServerDefault]
              ->OK.

Leave other settings as their default.

Assume that the client has a call request to server method SlowOperation that needs a relatively long time. We add this method with parameters to the interface:

[Right click on  ITestDefault in ClassView Window]
       ->Add Method->Method Name:[SlowOperation]
       ->Parameters: LONG* pClientThreadID,
                     LONG* pServerThreadID, LONG* pComObjectID
       ->OK.

In the source code generated by the wizard, add a data member: m_ClientThreadID as private, long type.

//////////////////////////////////////////////////////////////////
// CTestServerDefault
class ATL_NO_VTABLE CTestServerDefault :
  public CComObjectRootEx<CComSingleThreadModel>,
  public CComCoClass<CTestServerDefault,
         &CLSID_TestServerDefault>,
  public IDispatchImpl<ITestServerDefault,
         &IID_ITestServerDefault,
         &LIBID_LOCALSERVERDEFAULTLib>
{
public:
  CTestServerDefault():m_ClientThreadID(0)
  {
  }

DECLARE_REGISTRY_RESOURCEID(IDR_TESTSERVERDEFAULT)

DECLARE_PROTECT_FINAL_CONSTRUCT()

BEGIN_COM_MAP(CTestServerDefault)
  COM_INTERFACE_ENTRY(ITestServerDefault)
  COM_INTERFACE_ENTRY(IDispatch)
END_COM_MAP()

// ITestServerDefault
public:
  STDMETHOD(SlowOperation)(LONG* pClientThreadID,
                           LONG* pServerThreadID,
                           LONG* pComObjectID);
private:
  LONG m_ClientThreadID;
};

Method SlowOperation is implemented as this:

// TestServerDefault.cpp : Implementation of CTestServerDefault
STDMETHODIMP CTestServerDefault::SlowOperation(
             LONG *pClientThreadID,
             LONG *pServerThreadID,
             LONG *pComObjectID)
{
  // TODO: Add your implementation code here
  m_ClientThreadID = *pClientThreadID;
  Sleep(5000);
  *pClientThreadID=m_ClientThreadID;
  *pServerThreadID=GetCurrentThreadId();
  *pComObjectID=(long)(IUnknown*)(CComObject
                                  <CTestServerDefault>*)this;
  return S_OK;
}

The intention of function SlowOperation is to cache a client’s private data and return the stored data to the client sometime later. This function will be invoked by the client. It receives the client’s thread ID as an input parameter, stores it in its private member m_ClientThreadID, Sleeps five seconds, and then returns what it has stored back to the client, together with this server thread ID and the CTestServerDefault‘s COM object ID pComObjectID.

The pComObjectID, normally referred to as “COM Identity,” is an IUnknown pointer to the server’s COM Object. When a client calls ::CoCreateInstance to ask for a pointer to COM object, the COM system (“COM” for simplicity) will ask for this interface pointer pComObjectID in function call IClassfactory::CreateInstance to the server. Then, this interface pointer will be marshaled and returned to the client. Note that the interface the pointer client has obtained from ::CoCreateInstance is not p ComObjectID in the server process, but a interface pointer to the proxy of ITestServerDefault in client process as a result of Interface Marshalling1.

Now, we implement our client project of the console application, which will be used to test both the default local server and auto-threaded local server. The client process produces three threads. All threads invoke ::CoInitializeEx with parameter COINIT_MULTITHREADED to ask COM to create an MTA for this process. The main thread invokes CoCreateInstance to ask for ITestServerDefault pointer related to the COM object. COM responds with a series of operations in both processes of client and server; that is, launching the local server, waiting for the local server to register the class object, creating stub and proxy objects, creating RPC channel …, finally passing proxy’s ITestServerDefault pointer to the client’s variable pServer. If all these operations are done successfully, the main thread creates other two working threads within an interval of one second and passes the new threads this interface pointer. The two newly created threads will go into the same MTA created by the main thread, and use this passed-in interface pointer to access the remote COM server object. Because the client process invokes CoCreateInstance once, only one server object instance is created for both client threads to access. A CTimeTicker class is added to testing code to mark time elapsed; it is omitted here for simplicity.

// Client.cpp: Defines the entry point for the console application.
//
//comment this to test server without AutoThreadModule
//#define _MY_AUTO_THREAD
....
DWORD WINAPI threadFunc(LPVOID lpPara)
{
  CoInitializeEx(NULL,COINIT_MULTITHREADED);
  cout<<"start Thread Id="<<GetCurrentThreadId()<<endl;
#ifndef _MY_AUTO_THREAD
  ITestServerDefault* pServer= (ITestServerDefault*)lpPara;
#else
  ITestServerAutoThread* pServer= (ITestServerAutoThread*)lpPara;
#endif
  long clientThreadId=(long)GetCurrentThreadId();
  long serverThreadId=0;
  long ComObjId=0;
  try
  {
    cout<<"Client Thread Id(pass in):"
        <<clientThreadId<<endl;
    pServer->SlowOperation(&clientThreadId,
                           &serverThreadId,&ComObjId);
    cout<<"ServrResponded: ClientThreadId="
        <<clientThreadId<<endl;
    cout<<";ServerThreadId="
        <<serverThreadId<<"    COMObjectId="
        <<ComObjId<<endl;

  }
  catch(_com_error &e)
  {
    cout<<e.ErrorMessage()<<endl;
  }
  CoUninitialize();
  return 0;
}
//main thread
int main(int argc, char* argv[])
{
  CTimeTicker ticker;
  CoInitializeEx(NULL,COINIT_MULTITHREADED);
    //MTA apartment has 3 threads
  ticker.StartTicker();
#ifndef _MY_AUTO_THREAD
  ITestServerDefault * pServer;
  HRESULT hRes=CoCreateInstance(CLSID_TestServerDefault,NULL,
    CLSCTX_ALL,IID_ITestServerDefault,(void**)&pServer);
#else
  ITestServerAutoThread * pServer;
  HRESULT hRes=CoCreateInstance(CLSID_TestServerAutoThread,NULL,
    CLSCTX_ALL,IID_ITestServerAutoThread,(void**)&pServer);
#endif
  if(SUCCEEDED(hRes))
  {
    HANDLE hThread1=CreateThread(NULL,0,threadFunc,pServer,0,NULL);
    Sleep(1000);
    HANDLE hThread2=CreateThread(NULL,0,threadFunc,pServer,0,NULL);
    WaitForSingleObject(hThread1,INFINITE);
    WaitForSingleObject(hThread2,INFINITE);
    CloseHandle(hThread1);
    CloseHandle(hThread2);
    pServer->Release();

  }
  ticker.EndTicker();
  CoUninitialize();
  cout<<endl<<"main() ended"<<endl;
  return 0;
}

Also, don’t forget to add to stdafx.h to the Client project; otherwise, codes cannot pass the compiler.

// stdafx.h : include file for standard system include files,
....
#define _WIN32_WINNT 0x0400

Why We Need a Single Threaded Apartment

In normal multi-thread programming, a class instance can be accessed by multiple threads of same the process; therefore, coding of the concurrency control is needed on the class. To relieve the programmer from implementing this coding, STA is introduced to bind a class instance to a particular thread. Whereas if the class object itself is multi-threaded safe, it still needs to enter an MTA to utilize the COM system service such as dynamic activation and function call remoting. MTA releases a COM object, its thread affinity, by allowing it to be accessed by multiple threads just like a normal class instance in a multi-threaded surrounding. In this test, we will check the behavior of a normal COM object generated by a wizard and put it in an MTA apartment, rather than a STA apartment.

To make an ITestServerDefault object created in a MTA in local server, we need to change the server default behavior by modifying stdafx.h of the LocalServerDefault project.

// stdafx.h : include file for standard system include files,
.....
#ifndef _WIN32_WINNT
#define _WIN32_WINNT 0x0400
#endif
//#define _ATL_APARTMENT_THREADED    //default line
#define _ATL_FREE_THREADED           //add this line to generate
                                     //an MTA
#include <atlbase.h>
....

This modification will cause the local server to enter an MTA apartment due to following code:

// LocalServerDefault.cpp : Implementation of WinMain
.....
//////////////////////////////////////////////////////////////////
//
extern "C" int WINAPI _tWinMain(HINSTANCE hInstance,
    HINSTANCE /*hPrevInstance*/, LPTSTR lpCmdLine,
               int /*nShowCmd*/)
{
    lpCmdLine = GetCommandLine();
    //this line is necessary for _ATL_MIN_CRT

#if _WIN32_WINNT >= 0x0400 & defined(_ATL_FREE_THREADED)
    HRESULT hRes = CoInitializeEx(NULL, COINIT_MULTITHREADED);
    //enter an MTA
#else
    HRESULT hRes = CoInitialize(NULL);
#endif
.....

Figure 1 shows the result of two client threads accessing a ITestServerDefault object in server MTA apartment. Let’s explain it in detail. When client process 1720‘s main thread invoked ::CoCreateInstance and obtained an interface pointer pServer, COM has finished creating server object 12398280 and its Interface Pointer Marshalling. The Interface Pointer Marshalling process includes creation of the stub object of server object 12398280, an RPC channel of ITestServerDefault between the client and server process and a client proxy that pServer pointed to.

At time 12:23(mm:ss), the first client working thread 2020 raised a function call request SlowOperation via the pointer pServer to the client proxy. The proxy converted the SlowOperation call together with its parameter into a stream (Function Call Marshal), according to the call syntax of the ITestServerDefault information in the type library and sent the stream buffer to the RPC channel. At the other end of the RPC channel, the server process received the stream, and designated thread 3264 from the RPC Thread Pool to implement all tasks in server the client requests. Thread 3264 passed the stream as a parameter to the stub of COM object 12398280; the stub interpreted the stream and converted it to a function call (Function Call Unmarshal) according the same info in type library and called the COM object 12398280.

After the function call returned, the return value and out parameters were put in the stream buffer and returned to client thread 2020, which was still waiting on the RPC channel for the return value. Finally, the proxy retrieved the call result from the returned stream and passed the result value and out parameters to the original call of SlowOperation. The same procedure happened to the second SlowOperation call of client working thread 2252, except that another server thread, 2112, was designated from the RPC Thread Pool to process the call to the same COM object 12398280. Figure 1 illustrates this procedure.

Figure 1—MTA client accessing a local server MTA COM object

Now the problem is that the first client working thread, 2020, invoked the server service SlowOperation and cached its thread ID, but returned a wrong cached value later. The reason is that two server SlowOperation() function calls executed by server thread 3264 and 2112 at time interval 2:24 – 12:27 simultaneously accessed the same COM object 12398280, which has no implementation of concurrent protection on its private member used as cache. This is also the reason we need an STA apartment for the COM server.

Now, let’s change the server LocalServerDefault‘s apartment definition in StdAfx.h back to _ATL_APARTMENT_THREADED:

// stdafx.h
...
#define _ATL_APARTMENT_THREADED    //add this line
//#define _ATL_FREE_THREADED       //commend this line
#include <atlbase.h>
.....

With this definition, the local server’s main thread will invoke API function CoInitialize(NULL) to initialize an STA apartment to contain the COM object. A STA will create a hidden window. This is the reason the local server main thread needs to implement a Windows message pump:

//LocalServerDefault.cpp
...
MSG msg;
while (GetMessage(&msg, 0, 0, 0))
DispatchMessage(&msg);
....

After a client function call request goes through RPC channels, the RPC thread-pool thread mentioned above posts a message to the hidden window passing it the marshaled parameter data. All messages are serialized by the server’s windows message queue. The call request messages are dispatched one by one and transfer back to function call SlowOperation to the COM object. The second request will not be dispatched until the first SlowOperation() has returned. Figure 2 shows the screen capture.

Figure 2—MTA client accessing a local server STA COM object

The first client working thread, 2456, started at time 20:33; after 5 seconds, it got its correct cached data from the server. The second client thread, 3348, started one second later, but its call request had to wait another four seconds for thread 2456 and took nine seconds to finish the call request. The integrity of the COM object is guaranteed with the expense of additional time. Figure 2a illustrates this operation.

Figure 2a

So far, so good. By using the STA apartment mechanism, customer COM objects do not need to implement any line of concurrent code while concurrent accesses are still guaranteed. COM does that for us. Let’s proceed to the next section.

Why We Need an Auto-threaded COM Module

With the previous application, let’s start four client.exe applications simultaneously. Figure 3 gives screen captures of the first and last process.

Figure 3—Four Client Processes Access a Local STA Server Simultaneously (screen captures of the first and last client process)

The testing results are listed in Table 1. Two points need to be mentioned:

  • Four client processes used the same server process and same thread 4008, whereas each client has its own class instance. When the first client process 2452 executed CoCreateInstance, COM launched the server according to its Class ID CLSID_TestServerDefault in the system registry so that the server can register its Class Object to system table. This happened when the server main thread 4008 invoked ::CoRegisterClassObject. Subsequent CoCreateInstance calls would find this registered Class Object and then create their own class instance (COM Object) as the first client 2452 did. All these objects are created within the same STA of thread 4008.
  • Subsequent client processes’s function call requests had to wait for a longer time to be served. Because it is an STA apartment that the server created for COM objects, eight function call requests of four client process had to serialized to be executed by a single STA thread, 4008. Each client process takes 10 seconds to obtain a server response. So, the last process, 2256, took approximately 40 seconds to be served.
Client Process ID Thread ID Server Thread ID COM Object ID Time Needed (Sec)
2452 3876 4008 12398280 5
3032 10
1720 2012 4008 12398336 14
1180 19
2412 3176 4008 12398392 23
3032 33
2256 2996 4008 12398448 27
3876 37

Table 1—Times Needed for a Client Process to Obtain Server Responses When Four Client Processes Are Started Simultaneously

The shortcoming of this structure is that the only one COM server STA apartment is incompetent to serve multiple client processes concurrently even though each client process has its own set of proxy, RPC channel, and stub and class instances. Figure 3 illustrates this structure.

Figure 3

This is the reason why an auto-threaded COM Server Module is introduced. Let’s duplicate a new local server as the procedure stated above but with new server name LocalServerAutoThread and new interface name ITestServerAutoThread. Leave the implementation of SlowOperation the same as before.

To add Auto-thread implementation on LocalServerAutoThread, there are four easy steps modification on the MSDN article:

Figure 4—Four Client Processes Access an Auto-threaded Server Simultaneously (screen captures of the first and last client process)

Client Process ID Thread ID Server Thread ID COM Object ID Time Needed (Sec)
4136 3160 2292 12398568 5
3556 10
3848 2616 4144 12388264 5
2956 10
1940 2904 3680 12388320 5
2440 10
3876 3888 3484 12388376 5
3084 10

Table 2—Times Needed for Four Client Process to Access an Auto-threaded Server

With an Auto-threaded COM server, function calls from four simultaneously running client processes can be processed concurrently. For every client process, the server creates a different COM object and designates a different thread to serve it. Figure 4a shows this application structure.

Some Detail of Implementation of Auto-threaded Module in VC6

An auto-threaded COM server has a Thread Pool with a group of Thread-pool Threads. Thread-pool threads are initiated as STA apartments that are used to contain one or multiple COM objects.

When an Auto-threaded server is launched by the Service Control Manager (SCM), the main thread creates a global instance _Module, which is derived from class CComAutoThreadModule<>. CComAutoThreadModule< is the implementer of the Auto-threaded structure. It creates a thread pool in the server process.

//ATLBASE.H
template <class ThreadAllocator = CComSimpleThreadAllocator>
  //line3869
class CComAutoThreadModule : public CComModule
{
public:
  HRESULT Init(_ATL_OBJMAP_ENTRY* p, HINSTANCE h,
          const GUID* plibid = NULL, int nThreads
                             = GetDefaultThreads());
  ~CComAutoThreadModule();
  HRESULT CreateInstance(void* pfnCreateInstance,
                         REFIID riid, void** ppvObj);
  LONG Lock();
  LONG Unlock();
  DWORD dwThreadID;
  int m_nThreads;
  CComApartment* m_pApartments;
  ThreadAllocator m_Allocator;
  static int GetDefaultThreads()
  {
    SYSTEM_INFO si;
    GetSystemInfo(&si);
    return si.dwNumberOfProcessors * 4;
  }
};

// stdafx.h of LocalServerAutoThread
class CExeModule : public CComAutoThreadModule<>
{
public:
  LONG Unlock();
  DWORD dwThreadID;
  HANDLE hEventShutdown;
  void MonitorShutdown();
  bool StartMonitor();
  bool bActivity;
};

// LocalServerAutoThread.cpp : Implementation of WinMain

CExeModule _Module;    //create a global instance

extern "C" int WINAPI _tWinMain(HINSTANCE hInstance,
    HINSTANCE /*hPrevInstance*/, LPTSTR lpCmdLine,
               int /*nShowCmd*/)
{

#if _WIN32_WINNT >= 0x0400 & defined(_ATL_FREE_THREADED)
    HRESULT hRes = CoInitializeEx(NULL, COINIT_MULTITHREADED);
#else
    HRESULT hRes = CoInitialize(NULL);
#endif
    _ASSERTE(SUCCEEDED(hRes));
   //CComAutoThreadModule<> initialize here
    _Module.Init(ObjectMap, hInstance, &
                 LIBID_LOCALSERVERAUTOTHREADLib);
    _Module.dwThreadID = GetCurrentThreadId();
...

The auto-Threaded server uses two default parameters that we need to be concerned about here. One is the template parameter; the default is class CComSimpleThreadAllocator. When the server’s main thread needs to create a new COM object by IClassFactory::CreateInstance(), CComSimpleThreadAllocator is responsible for selecting a thread from thread pool, in which the COM object will be created. The selection of a thread from thread pool is conducted in Round-Robin sequence.

//Atlbase.h
class CComSimpleThreadAllocator    //line 3853
{
public:
  CComSimpleThreadAllocator()
  {
    m_nThread = 0;
  }
  //Round-Robin selection of thread-pool thread
  int GetThread(CComApartment* /*pApt*/, int nThreads)
  {
    if (++m_nThread == nThreads)
      m_nThread = 0;
    return m_nThread;
  }
  int m_nThread;    //number of thread in thread-pool
};

The second default parameter is the forth parameter of CComAutoThreadModule<>::Init(), which decides the number of threads in the thread pool.

  static int GetDefaultThreads()
  {
    SYSTEM_INFO si;
    GetSystemInfo(&si);
    return si.dwNumberOfProcessors * 4;    //1 processor for
                                           //normal PC
  }

If we don’t give this number, the default will be Number of CPU*4; in other words, four threads for a normal PC. We can customize this number, as shown in the following code:

// stdafx.h of LocalServerAutoThread
// _Module.Init(ObjectMap, hInstance,
    &LIBID_LOCALSERVERPOOLLib);        //4 thread-pool threads
    _Module.Init(ObjectMap, hInstance,
    &LIBID_LOCALSERVERPOOLLib,6);      //change to 6

With the number of threads decided, the server’s main STA creates these threads and register a message “ATL_CREATE_OBJECT”; this message will be used for the main STA to communicate with thread-pool threads.

//ATLCOM.H
template <class ThreadAllocator    //line 5132
inline HRESULT CComAutoThreadModule<ThreadAllocator>
       ::Init(_ATL_OBJMAP_ENTRY* p, HINSTANCE h,
       const GUID* plibid, int nThreads)
{
  m_nThreads = nThreads;
  m_pApartments = NULL;
  //create thread-pool
  ATLTRY(m_pApartments = new CComApartment[m_nThreads]);
  ATLASSERT(m_pApartments != NULL);
  if(m_pApartments == NULL)
    return E_OUTOFMEMORY;
  for (int i = 0; i < nThreads; i++)
    //create thread-pool threads
    m_pApartments[i].m_hThread = CreateThread(NULL, 0,
                                 CComApartment::_Apartment,
                                 (void*)&m_pApartments[i], 0,
                                 &m_pApartments[i].m_dwThreadID);
  CComApartment::ATL_CREATE_OBJECT
               = RegisterWindowMessage(_T("ATL_CREATE_OBJECT"));
  return CComModule::Init(p, h, plibid);
}

The thread-pool thread is implemented by CComApartment. Servermain thread initiates an array of CComApartment objects and executes the Apartment() member function in the procedure of new thread creation.

//ATLBASE.H
//note that code executed in thread-pool thread rather
//than main thread
DWORD CComApartment::Apartment()        //line 3816
{
  CoInitialize(NULL);                   //enter STA
  MSG msg;
  while(GetMessage(&msg, 0, 0, 0))      //waiting main STA message
  {
    if (msg.message == ATL_CREATE_OBJECT)
    {
      _AtlAptCreateObjData* pdata =
          (_AtlAptCreateObjData*)msg.lParam;
      IUnknown* pUnk = NULL;
      //create COM object in current apt.
      pdata->hRes = pdata->pfnCreateInstance(NULL,
            IID_IUnknown, (void**)&pUnk);
      if (SUCCEEDED(pdata->hRes))
        //interface pointer marshalling between current STA
        //and main STA
        pdata->hRes = CoMarshalInterThreadInterfaceInStream
                         (*pdata->piid, pUnk, &pdata->pStream);
      if (SUCCEEDED(pdata->hRes))
      {
        pUnk->Release();
        ATLTRACE2(atlTraceCOM, 2, _T("Object created on thread
                                      = %d\n"),
                                      GetCurrentThreadId());
      }
      SetEvent(pdata->hEvent);    //notify main STA
    }
    DispatchMessage(&msg);
  }
  CoUninitialize();
  return 0;
}

Apartment() initializes the newly created thread as a STA by CoInitialize(NULL). To distinguish from main thread STA, let’s refer to it as thread-pool STA. Thread-pool STA is blocked at function GetMessage() until the system invokes main thread STA’s IClassFactory::CreateInstance(). The call of IClassFactory::CreateInstance() happens in main STA because it is the main STA that registered the class object pointer to the COM system table. The main STA packs the COM object, creating function pointer pfnCreateInstance in message ATL_CREATE_OBJECT and sends it to a designated thread-pool STA.

//ATLCOM.H 
//code executed in main STA
HRESULT CComAutoThreadModule<ThreadAllocator>::
        CreateInstance(void* pfnCreateInstance,
        REFIID riid, void** ppvObj)    //line 5180
{
  _ATL_CREATORFUNC* pFunc = (_ATL_CREATORFUNC*) pfnCreateInstance;
  _AtlAptCreateObjData data;
  data.pfnCreateInstance = pFunc;
    //COM object create function pointer
  data.piid = &riid;
  data.hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
  data.hRes = S_OK;
  int nThread = m_Allocator.GetThread(m_pApartments, m_nThreads);
  //thread-pool thread selection
  //ask thread-pool STA to create object
  ::PostThreadMessage(m_pApartments[nThread].m_dwThreadID,
    CComApartment::ATL_CREATE_OBJECT, 0, (LPARAM)&data);
  AtlWaitWithMessageLoop(data.hEvent);
  CloseHandle(data.hEvent);
  if (SUCCEEDED(data.hRes))
  //unmarshal interface pointer
  data.hRes = CoGetInterfaceAndReleaseStream(data.pStream,
                                             riid, ppvObj);
  return data.hRes;
}

With COM object creation function pfnCreateInstance in message ATL_CREATE_OBJECT, the thread-pool STA creates an ITestServerAutoThread COM object, and marshals its interface pointer to main STA by invoking ::CoMarshalInterThreadInterfaceInStream, which will establish a channel between the thread-pool STA and the main STA. Subsequently, the main STA will un-marshal the interface pointer by ::CoGetInterfaceAndReleaseStream and pass it back to the COM system, which involves in another round of marshaling between the local server and the request client. With the RPC channel established between the pair of COM object and client, function call requests can be routed to the server and executed concurrently in different thread-pool STAs.

After clients have released their references to COM objects, the COM objects are destroyed. At the time the last reference count is released, CComAutoThreadModule object’s destructor is invoked. The destructor will terminate thread-pool STAs by posting them WM_QUIT messages.

//ATLCOM.H
template <class ThreadAllocator>    //line 5197
CComAutoThreadModule<ThreadAllocator>::~CComAutoThreadModule()
{
  for (int i=0; i < m_nThreads; i++)
  {
    //modified according to msdn BUG:Q202128
    //::PostThreadMessage(m_pApartments[i].m_dwThreadID,
                          WM_QUIT, 0, 0);
    while (::PostThreadMessage(m_pApartments[i].m_dwThreadID,
                          WM_QUIT, 0, 0) == 0) ::Sleep(100);
    ::WaitForSingleObject(m_pApartments[i].m_hThread, INFINITE);
  }
  delete[] m_pApartments;
}

Conclusion

A single Threaded Apartment protects a COM object’s data integrity by preventing multiple threads from accessing it. However, a STA also prevents multiple threads from concurrently accessing their respective COM objects if these objects are created in a single STA, which is the default local server mode. The Auto Threaded Server Module allows multiple instances of the client application to access the same local server simultaneously by creating a thread pool to contain multiple COM objects in separated STA apartments.

1 For more detail on interface pointer marshalling and function call marshalling, please refer to
the section “Concurrency Management: Threading Models,” “DCOM ArchitectureMarkus Horstmann and Mary Kirtland, MSDN, 1997

Chapter 5, “Apartments,”Essential COM DON BOX, Addison-Wesley Pub Co, 1997

Downloads

Download VC6 source – 50 Kb

Download VC7 source – 52 Kb

More by Author

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Must Read