Introduction
This article is written for advanced users who knows STA COM apartments inside out and it will not explain the basic COM concepts here. If you need to grasp the basic grounding of COM, you can read the beginner COM articles available on The Code Project. Many a time, when we call a COM object function which takes a long time to complete, our main thread and UI are stalled by this function, until the function completes its operation. Then we attempt to use Global Interface Table or CoMarshalInterThreadInterfaceInStream()
to marshal the interface pointer to another thread, only to find out it doesn't work at all. The main thread remains stalled while the lengthy function is executing. The reason is because what you had marshaled to another thread, is only an interface proxy, the real work is still done by the STA object which is CoCreateInstance
'd in the main thread. STA objects have thread affinity. This article will walk you through five MFC clients using a really simple COM object.
Background
I used to write and use a lot of free-threaded and both-threaded objects before I came to my present company where they have written and used a lot of STA objects. STA objects are a hassle to use when you are writing a multi-threading application because they are not to be used across different STA apartments discriminately, whereas MTA objects do not have this problem; they can be used in different threads in MTA apartment without marshalling though you have to do your own synchronizing internally. STA objects and apartments is a much complex beast than MTA objects and apartment. Microsoft designed a lot of rules for STA which we have to follow, if it is to guarantee that our STA objects will work correctly.
Let us begin our five short examples now.
Our COM Server
For simplicity, we will use a really simple in-process DLL server whose methods will take no parameters.
STDMETHODIMP CTest::Initialize(void)
{
Sleep(5000);
return S_OK;
}
STDMETHODIMP CTest::AnotherLongOperation(void)
{
Sleep(5000);
return S_OK;
}
STDMETHODIMP CTest::ShortOperation(void)
{
Sleep(50);
return S_OK;
}
Remember you have to build the server first before you build the client applications and run them.
We will use Sleep()
to indicate how much time will elapse before the function will return.
Our 1st Client
Our 1st client application will CoCreateInstance()
the ITest
object and call a simulated lengthy Initialize()
function. As you can see from the Initialize
function above, Initialize()
will only return 5
seconds later, meaning your dialog will not appear until 5
seconds has passed. We also have a "Long Operation" button which calls ITest
's AnotherLongOperation()
. When this button is clicked, your dialog becomes 'hanged' and unresponsive. To know whether the UI is still responsive, try to drag the dialog around by clicking and holding and moving the dialog title. If it is unresponsive, you cannot move the dialog.
BOOL CTestClient1Dlg::OnInitDialog()
{
HRESULT hr = m_ptrTest.CoCreateInstance(CLSID_Test,NULL,CLSCTX_INPROC_SERVER);
if( SUCCEEDED(hr) )
m_ptrTest->Initialize();
else
MessageBox(_T("CoCreateInstance() fails"),MB_OK);
return TRUE;
}
void CTestClient1Dlg::OnBnClickedBtnLongop()
{
CWaitCursor wait;
if(m_ptrTest)
{
m_ptrTest->AnotherLongOperation();
MessageBox(_T("Done"));
}
else
MessageBox(_T("Invalid pointer"),MB_OK);
}
Our 2nd Client
For our 2nd client application, we will use Global Interface Table(GIT) to marshal the pointer. I could also use CoMarshalInterThreadInterfaceInStream()
but GIT is much simpler to use and achieves the same result of marshalling. Now the Initialize
function and AnotherLongOperation
function are both called in a worker thread. When you run this application, the UI comes up immediately because the Initialize()
is called in a separate secondary thread now. But UI still becomes 'hanged' because as I had said earlier, the main work is still being done by the COM object in a main UI thread in which it is CoCreateInstance
'd. The worker thread will post a WM_DONE
message to the main thread, when the called Initialize
and AnotherLongOperation()
finish their operation..
BOOL CTestClient2Dlg::OnInitDialog()
{
HRESULT hr = m_ptrTest.CoCreateInstance(CLSID_Test,NULL,CLSCTX_INPROC_SERVER);
if( SUCCEEDED(hr) )
{
hr = g_pGIT.CoCreateInstance(CLSID_StdGlobalInterfaceTable,
NULL,
CLSCTX_INPROC_SERVER );
if( FAILED(hr) )
{
MessageBox(_T("CoCreateInstance Global Interface Table fails"),MB_OK);
return TRUE;
}
hr = g_pGIT->RegisterInterfaceInGlobal(m_ptrTest, IID_ITest, &g_dwCookie);
if( FAILED(hr) )
{
MessageBox(_T("Register Interface in GIT fails"),MB_OK);
return TRUE;
}
AfxBeginThread( ThreadProc1, NULL );
}
else
MessageBox(_T("CoCreateInstance() fails"),MB_OK);
return TRUE;
}
void CTestClient2Dlg::OnBnClickedBtnlongop()
{
CWaitCursor wait;
if(m_ptrTest)
AfxBeginThread( ThreadProc2, NULL );
else
MessageBox(_T("Invalid pointer"),MB_OK);
}
void CTestClient2Dlg::OnDestroy()
{
CDialog::OnDestroy();
HRESULT hr=E_FAIL;
if( g_pGIT )
hr = g_pGIT->RevokeInterfaceFromGlobal(g_dwCookie);
}
LRESULT CTestClient2Dlg::OnDone(WPARAM wParam, LPARAM lParam)
{
MessageBox(_T("Done"));
return 0;
}
UINT ThreadProc1( LPVOID pParam )
{
::CoInitialize(NULL);
{
CComPtr<iTest> ptrTest;
HRESULT hr=E_FAIL;
if( g_pGIT )
hr = g_pGIT->GetInterfaceFromGlobal
(g_dwCookie, IID_ITest, (void**)&ptrTest );
if( SUCCEEDED(hr) && ptrTest )
ptrTest->Initialize();
}
::CoUninitialize();
AfxGetMainWnd()->PostMessage(WM_DONE);
return 0;
}
UINT ThreadProc2( LPVOID pParam )
{
::CoInitialize(NULL);
{
CComPtr<iTest> ptrTest;
HRESULT hr=E_FAIL;
if( g_pGIT )
hr = g_pGIT->GetInterfaceFromGlobal
(g_dwCookie, IID_ITest, (void**)&ptrTest );
if( SUCCEEDED(hr) && ptrTest )
ptrTest->AnotherLongOperation();
}
::CoUninitialize();
AfxGetMainWnd()->PostMessage(WM_DONE);
return 0;
}
Our 3rd Client
For our 3rd client application, we will not marshal the ITest
pointer to the worker threads and bypass the COM STA rules. Surprisingly it works. The UI thread is not stalled when either of the Initialize()
and AnotherLongOperation()
is called. Unfortunately, this wrong approach is used by many COM programmers to resolve this problem. If you do something like this, first of all, STA thread safety rules are bypassed. The rule which all STA calls to the same object are serialized, is violated. In other words, You have to do your own thread synchronizing, if you chose this approach. Secondly, this method will not work in Windows Server OSes and Windows XP Tablet Edition which the COM STA rules are enforced, the Initialize()
and AnotherLongOperation()
may return RPC_E_WRONG_THREAD
on these OSes.
BOOL CTestClient3Dlg::OnInitDialog()
{
HRESULT hr = m_ptrTest.CoCreateInstance(CLSID_Test,NULL,CLSCTX_INPROC_SERVER);
if( SUCCEEDED(hr) )
{
AfxBeginThread( ThreadProc1, (void*)(ITest*)m_ptrTest );
}
else
MessageBox(_T("CoCreateInstance() fails"),MB_OK);
return TRUE;
}
void CTestClient3Dlg::OnBnClickedBtnlongop()
{
CWaitCursor wait;
if(m_ptrTest)
AfxBeginThread( ThreadProc2, (void*)(ITest*)m_ptrTest );
else
MessageBox(_T("Invalid pointer"),MB_OK);
}
LRESULT CTestClient3Dlg::OnDone(WPARAM wParam, LPARAM lParam)
{
MessageBox(_T("Done"));
return 0;
}
UINT ThreadProc1( LPVOID pParam )
{
::CoInitialize(NULL);
{
CComPtr<iTest> ptrTest = (ITest*)(pParam);
if( ptrTest )
ptrTest->Initialize();
}
::CoUninitialize();
AfxGetMainWnd()->PostMessage(WM_DONE);
return 0;
}
UINT ThreadProc2( LPVOID pParam )
{
::CoInitialize(NULL);
{
CComPtr<iTest> ptrTest = (ITest*)(pParam);
if( ptrTest )
ptrTest->AnotherLongOperation();
}
::CoUninitialize();
AfxGetMainWnd()->PostMessage(WM_DONE);
return 0;
}
Our 4th Client
For our 4th client application, we will CoCreateInstance()
the ITest
in a secondary thread which is a UI thread; a worker thread which contains a message loop, is known as a UI thread. A message loop is needed because the COM object will be called from the main thread. After the COM object is created successfully, it will be marshaled and post a message to the main thread to unmarshal it. For any operation which takes a long time, like AnotherLongOperation()
, we will post a custom message to the secondary thread to execute the function and post a done message to the main UI thread when it is finished. For other functions which do not take up a lot of time, like ShortOperation()
, we will call them like any normal function, as shown in the example below:
void CTestClient4Dlg::OnBnClickedBtnnormalop()
{
if( m_ptrTest )
{
m_ptrTest->ShortOperation();
MessageBox(L"Done", L"Msg", MB_OK );
}
else
MessageBox(L"Invalid pointer", L"Error", MB_OK );
}
Here is rest of the code of the 4th client. Before the secondary thread ends, it will revoke the interface pointer from the Global Interface Table.
BOOL CTestClient4Dlg::OnInitDialog()
{
CWnd *pWnd = GetDlgItem(IDC_BTNLONGOP);
if( pWnd ) pWnd->EnableWindow(FALSE);
pWnd = GetDlgItem(IDC_BTNNORMALOP);
if( pWnd ) pWnd->EnableWindow(FALSE);
m_evtExit = CreateEvent(NULL,TRUE,FALSE,NULL);
g_evtExit = m_evtExit;
m_pThread = AfxBeginThread( ThreadProc, NULL );
return TRUE;
}
void CTestClient4Dlg::OnBnClickedBtnlongop()
{
if( m_bThreadCreated )
m_pThread->PostThreadMessage(WM_LONGOP, 0, 0 );
}
LRESULT CTestClient4Dlg::OnDone(WPARAM wParam, LPARAM lParam)
{
if( !m_ptrTest )
{
HRESULT hr=E_FAIL;
if( g_pGIT )
hr = g_pGIT->GetInterfaceFromGlobal
(g_dwCookie, IID_ITest, (void**)&m_ptrTest );
m_bThreadCreated = true;
}
MessageBox(_T("Done"));
CWnd *pWnd = GetDlgItem(IDC_BTNLONGOP);
if( pWnd ) pWnd->EnableWindow(TRUE);
pWnd = GetDlgItem(IDC_BTNNORMALOP);
if( pWnd ) pWnd->EnableWindow(TRUE);
return 0;
}
void CTestClient4Dlg::OnDestroy()
{
CDialog::OnDestroy();
if( m_ptrTest )
m_ptrTest.Release();
SetEvent(m_evtExit);
m_pThread->PostThreadMessage(WM_QUIT, 0, 0 );
WaitForSingleObject(m_pThread->m_hThread, INFINITE );
}
void CTestClient4Dlg::OnBnClickedBtnnormalop()
{
if( m_ptrTest )
{
m_ptrTest->ShortOperation();
MessageBox(L"Done", L"Msg", MB_OK );
}
else
MessageBox(L"Invalid pointer", L"Error", MB_OK );
}
UINT ThreadProc( LPVOID pParam )
{
::CoInitialize(NULL);
{
CComPtr<iTest> ptrTest;
HRESULT hr = ptrTest.CoCreateInstance(CLSID_Test,NULL,CLSCTX_INPROC_SERVER);
ptrTest->Initialize();
if( SUCCEEDED(hr) )
{
hr = g_pGIT.CoCreateInstance(CLSID_StdGlobalInterfaceTable,
NULL,
CLSCTX_INPROC_SERVER );
if( FAILED(hr) )
{
return 1;
}
hr = g_pGIT->RegisterInterfaceInGlobal(ptrTest, IID_ITest, &g_dwCookie);
if( FAILED(hr) )
{
return 1;
}
AfxGetMainWnd()->PostMessage(WM_DONE);
}
else
return 1;
MSG msg ;
while(true)
{
DWORD result =
MsgWaitForMultipleObjects(1,&g_evtExit,FALSE,INFINITE,QS_ALLINPUT);
if (result == (WAIT_OBJECT_0 + 1))
{
PeekMessage(&msg, NULL, 0, 0, PM_REMOVE);
if (msg.message == WM_QUIT)
{
HRESULT hr=E_FAIL;
if( g_pGIT )
hr = g_pGIT->RevokeInterfaceFromGlobal(g_dwCookie);
TRACE(_T("Thread ends2\n"));
::CoUninitialize();
return 1;
}
if (msg.message == WM_LONGOP)
{
if( ptrTest )
ptrTest->AnotherLongOperation();
AfxGetMainWnd()->PostMessage(WM_DONE);
}
DispatchMessage(&msg);
}
else
{
HRESULT hr=E_FAIL;
if( g_pGIT )
hr = g_pGIT->RevokeInterfaceFromGlobal(g_dwCookie);
TRACE(_T("Thread ends2\n"));
::CoUninitialize();
return 1;
}
}
}
::CoUninitialize();
return 0;
}
One disadvantage of this approach is that you have to ensure the secondary thread lives as long as you want to use the STA object which is CoCreateInstance
'd in the secondary thread.
Our 5th Client
For our 4th client application, we use CWinThread
's PostThreadMessage()
to post custom message to the secondary thread (which is created by AfxBeginThread()
) to call lengthy operation. If we are not using MFC but pure Win32 API, we do not have the luxury of using PostThreadMessage()
of the CWinThread
class. The 5th Client is still based on MFC but I will show you how to write the Win32 UI thread in such a way that it can receive Win32 messages.
We will create and register a Window
class (not a real class in the literal sense), a default window procedure for the Window
class and then create a transparent window and a message loop to receive messages. The secondary thread will be created by the Win32 CreateThread()
function. Other than these differences, the 5th client application is similar to the 4th client application. Please note the 5th client application is similar to the situation when you create an STA object in an MTA apartment, the STA will be created in another thread with a transparent window to receive a message when its methods are called.
BOOL CTestClient5Dlg::OnInitDialog()
{
CWnd *pWnd = GetDlgItem(IDC_BTNLONGOP);
if( pWnd ) pWnd->EnableWindow(FALSE);
pWnd = GetDlgItem(IDC_BTNNORMALOP);
if( pWnd ) pWnd->EnableWindow(FALSE);
m_evtExit = CreateEvent(NULL,TRUE,FALSE,NULL);
g_evtExit = m_evtExit;
m_hThread = CreateThread( NULL, 10000, ThreadProc, GetSafeHwnd(), 0, &m_dwThreadID );
return TRUE;
}
void CTestClient5Dlg::OnBnClickedBtnlongop()
{
if( m_bThreadCreated )
::PostMessage(g_Hwnd, WM_LONGOP, 0, 0 );
}
LRESULT CTestClient5Dlg::OnDone(WPARAM wParam, LPARAM lParam)
{
if( !m_ptrTest )
{
HRESULT hr=E_FAIL;
if( g_pGIT )
hr = g_pGIT->GetInterfaceFromGlobal
(g_dwCookie, IID_ITest, (void**)&m_ptrTest );
m_bThreadCreated = true;
}
MessageBox(_T("Done"));
CWnd *pWnd = GetDlgItem(IDC_BTNLONGOP);
if( pWnd ) pWnd->EnableWindow(TRUE);
pWnd = GetDlgItem(IDC_BTNNORMALOP);
if( pWnd ) pWnd->EnableWindow(TRUE);
return 0;
}
void CTestClient5Dlg::OnDestroy()
{
CDialog::OnDestroy();
if( m_ptrTest )
m_ptrTest.Release();
SetEvent(m_evtExit);
::PostMessage(g_Hwnd, WM_QUIT, 0, 0 );
WaitForSingleObject(m_hThread, INFINITE );
}
void CTestClient5Dlg::OnBnClickedBtnnormalop()
{
if( m_ptrTest )
{
m_ptrTest->ShortOperation();
MessageBox(L"Done", L"Msg", MB_OK );
}
else
MessageBox(L"Invalid pointer", L"Error", MB_OK );
}
DWORD WINAPI ThreadProc( LPVOID pParam )
{
HWND hwndParent = (HWND)(pParam);
::CoInitialize(NULL);
{
CComPtr<iTest> ptrTest;
HRESULT hr = ptrTest.CoCreateInstance(CLSID_Test,NULL,CLSCTX_INPROC_SERVER);
ptrTest->Initialize();
if( SUCCEEDED(hr) )
{
hr = g_pGIT.CoCreateInstance(CLSID_StdGlobalInterfaceTable,
NULL,
CLSCTX_INPROC_SERVER );
if( FAILED(hr) )
{
return 1;
}
hr = g_pGIT->RegisterInterfaceInGlobal(ptrTest, IID_ITest, &g_dwCookie);
if( FAILED(hr) )
{
return 1;
}
::PostMessage(hwndParent, WM_DONE, 0, 0 );
}
else
return 1;
static wchar_t strAppName[]=L"Some window";
WNDCLASSEX wc;
memset(&wc,0,sizeof(wc));
wc.cbSize=sizeof(WNDCLASSEX);
wc.style=CS_HREDRAW | CS_VREDRAW | CS_OWNDC | CS_DBLCLKS;
wc.cbClsExtra=0;
wc.cbWndExtra=0;
wc.lpfnWndProc = WndProc;
wc.hInstance=NULL;
wc.hbrBackground=(HBRUSH)GetStockObject( DKGRAY_BRUSH );
wc.hIcon=LoadIcon(NULL, IDI_APPLICATION);
wc.hIconSm=LoadIcon(NULL, IDI_APPLICATION);
wc.hCursor=LoadCursor(NULL,IDC_CROSS);
wc.lpszMenuName = NULL;
wc.lpszClassName=strAppName;
RegisterClassEx(&wc);
g_Hwnd=CreateWindowEx(NULL,
strAppName,
strAppName,
WS_OVERLAPPEDWINDOW|WS_EX_TRANSPARENT,
CW_USEDEFAULT,
CW_USEDEFAULT,
512,512,
NULL,
NULL,
NULL,
NULL);
ShowWindow(g_Hwnd,SW_HIDE);
while(true)
{
DWORD result =
MsgWaitForMultipleObjects(1,&g_evtExit,FALSE,INFINITE,QS_ALLINPUT);
if (result == (WAIT_OBJECT_0 + 1))
{
PeekMessage(&msg, NULL, 0, 0, PM_REMOVE);
if (msg.message == WM_QUIT)
{
HRESULT hr=E_FAIL;
if( g_pGIT )
hr = g_pGIT->RevokeInterfaceFromGlobal(g_dwCookie);
TRACE(_T("Thread ends2\n"));
::CoUninitialize();
return 1;
}
if (msg.message == WM_LONGOP)
{
if( ptrTest )
ptrTest->AnotherLongOperation();
::PostMessage(hwndParent, WM_DONE, 0, 0 );
}
DispatchMessage(&msg);
}
else
{
HRESULT hr=E_FAIL;
if( g_pGIT )
hr = g_pGIT->RevokeInterfaceFromGlobal(g_dwCookie);
TRACE(_T("Thread ends2\n"));
::CoUninitialize();
return 1;
}
}
}
::CoUninitialize();
return 0;
}
long CALLBACK WndProc(HWND hWnd, UINT uMessage, WPARAM wParam, LPARAM lParam)
{
switch(uMessage)
{
case WM_DESTROY:
{
PostQuitMessage(0);
return 0;
}
default:
{
return DefWindowProc(hWnd, uMessage, wParam, lParam);
}
}
}
Please note that the 5th client also has the same disadvantage that you have to ensure the secondary thread lives as long as you want to use the STA object which is CoCreateInstance
'd in the secondary thread. If the COM object which you used, exposes other member COM objects, you have to marshal them as well if you want to use them in another thread. What about member COM objects which are not exposed? This is alright because the marshalled object is always executed in the thread where it is CoCreateInstance
'd, so its unexposed member COM objects would always be executed in that thread.
Note: There is a fix in the previous code where it use PeekMessage
as the message pump which utilizes 100% of the CPU time. I tried to use GetMessage
, apparently, GetMessage
doesn't work in a MFC code (But the GetMessage
solution can work in a C++/CLI code). So I have to resort to using MsgWaitForMultipleObjects
to resolve it.
Other Solutions
Another solution to this problem is to do multi-threading inside the COM STA object but sometimes you do not have access to the source code of the STA class. Lastly, if you have access to the source code of COM class, you can choose to write an MTA class.
Conclusion
This is a short article with simple examples to demonstrate the five scenarios. I used a simple COM server and client application because I don't want my readers to be distracted by a needlessly complex example, from the information I want to convey in this article. Another reason is I do not like to write long articles. I hope this article will not get a lousy rating because of these reasons.
This article is dedicated to Mr Lim Bio Liong, my fellow CP'ian from Singapore and a COM expert whose STA articles on The Code Project have helped me a lot in understanding COM STA objects and apartments.
References
History
- 29th September, 2008, Fixed the problem of PeekMessage() taking 100% of the CPU time by using MsgWaitForMultipleObjects
- 7th May, 2008, First release