Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Use STA COM Objects Asynchronously

0.00/5 (No votes)
1 Oct 2008 1  
How to use STA COM objects asynchronously without blocking your main thread

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;  // return TRUE  unless you set the focus to a control
}

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;  // return TRUE  unless you set the focus to a control
}

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;  // return TRUE  unless you set the focus to a control
}

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;  // return TRUE  unless you set the focus to a control
}

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 it's a quit message, we're out of here.
                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);
                }
                // Otherwise, dispatch the message.
                DispatchMessage(&msg); 
            }
            else
            {
                // Quit event
                HRESULT hr=E_FAIL;
                if( g_pGIT )
                    hr = g_pGIT->RevokeInterfaceFromGlobal(g_dwCookie);
                    TRACE(_T("Thread ends2\n"));
                    ::CoUninitialize();
                return 1;
            }
        } // End of PeekMessage while loop.

    }
    ::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;  // return TRUE  unless you set the focus to a control
}

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; //The window class used to create our window
        memset(&wc,0,sizeof(wc));

        //Fill in the windows class
        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;

        //Register the class with windows
        RegisterClassEx(&wc);

        //Create a windows based on the previous class
        g_Hwnd=CreateWindowEx(NULL,//Advanced style settings
                            strAppName,//class name
                            strAppName,//window caption
                            WS_OVERLAPPEDWINDOW|WS_EX_TRANSPARENT,//Windows style
                            CW_USEDEFAULT,//initial x position
                            CW_USEDEFAULT,//initial y position
                            512,512,//Initial width and height
                            NULL,//Handle to the parent windows
                            NULL,//Handle to the menu
                            NULL,//Handle to the app instance
                            NULL);//Advanced context

        //Display the windows
        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 it's a quit message, we're out of here.
                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 );
                }
                // Otherwise, dispatch the message.
                DispatchMessage(&msg); 
            }
            else
            {
                // Quit event
                HRESULT hr=E_FAIL;
                if( g_pGIT )
                    hr = g_pGIT->RevokeInterfaceFromGlobal(g_dwCookie);
                    TRACE(_T("Thread ends2\n"));
                    ::CoUninitialize();
                return 1;
            }
        } // End of PeekMessage while loop.

    }
    ::CoUninitialize();

    return 0;
}

long CALLBACK WndProc(HWND hWnd, UINT uMessage, WPARAM wParam, LPARAM lParam)
{
    // Switch the windows message to figure out what it is
    switch(uMessage)
    {
        case WM_DESTROY:
        {
            //Tell windows to put a WM_QUIT message in our message queue
            PostQuitMessage(0);
            return 0;
        }
        default:
        {
            //Let windows handle this message
            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

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here