Use STA COM Objects Asynchronously

Introduction

This article is written for advanced users who knows STA COM apartments inside out; it will not explain the basic COM concepts. If you need to grasp the basic grounding of COM, you can read the beginner COM articles available on CodeGuru. Many times, when you call a COM object function that takes a long time to complete, your main thread and UI are stalled by this function until the function completes its operation. Then, you will attempt to use a 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 that 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 free-threaded and both-threaded objects before I came to my present company; 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 MTA apartments without marshalling although you have to do your own synchronizing internally. Using STA objects and apartments is a much complex beast than MTA objects and apartments. Microsoft designed a lot of rules for STA that you have to follow, if only to guarantee that STA objects will work correctly.

Here are the five short examples.

The COM Server

For simplicity, I 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.

I will use Sleep() to indicate a how much time will elapse before the function will return.

The First Client

The first 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 return only five seconds later; this means that your dialog will not appear until five seconds has passed. You also have a "Long Operation" button that calls ITest's AnotherLongOperation(). When this button is clicked, your dialog 'hangs' and becomes unresponsive. To know whether the UI is still responsive, try to drag the dialog around by clicking, 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 unless you set the focus to a control
   return TRUE;
}

void CTestClient1Dlg::OnBnClickedBtnLongop()
{
   CWaitCursor wait;

   if(m_ptrTest)
   {
      m_ptrTest->AnotherLongOperation();
      MessageBox(_T("Done"));
   }
   else
      MessageBox(_T("Invalid pointer"),MB_OK);
}

Use STA COM Objects Asynchronously

The Second Client

For the second client application, you will use the Global Interface Table (GIT) to marshal the pointer. You also could use CoMarshalInterThreadInterfaceInStream(), but GIT is much simpler to use and achieves the same result of marshalling. Now, the Initialize function and AnotherLongOperation funtion are both called in a worker thread. When you run this application, the UI comes up immediately because Initialize() is called in a separate secondary thread now. But, the UI still becomes 'hung' because, as I 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 unless you set the focus to a control
   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;
}

Use STA COM Objects Asynchronously

The Third Client

For the third client application, you 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 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 that says 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 in 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 unless you set the focus to a control
   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;
}

Use STA COM Objects Asynchronously

The Fourth Client

For the fourth client application, you will CoCreateInstance() the ITest in secondary thread, which is a UI thread. A worker thread that 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 posted as a message to the main thread to unmarshal it. For any operation that takes a long time, like AnotherLongOperation(), you 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 that do not take up a lot of time, such as ShortOperation(), you will call them like any normal function, as in the following example.

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 fourth 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 that is CoCreateInstance'd in the secondary thread.

Use STA COM Objects Asynchronously

The Fifth Client

For the fourth client application, you used CWinThread's PostThreadMessage() to post a custom message to the secondary thread (which is created by AfxBeginThread()) to call a lengthy operation. If you are not using MFC but pure Win32 API, you do not have the luxury of using PostThreadMessage() of the CWinThread class. The fifth 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.

You will create and register a Window class (not a real class in 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 fifth client application is similar to the fourth client application. Please note that the fifth client application is similar to the situation when you create a STA object in a MTA apartment; the STA will be created in another thread with a transparent window to receive a message when its method is 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);
        }
    }
}
Note: The fifth client also has the same disadvantage in that you have to ensure the secondary thread lives as long as you want to use the STA object that is CoCreateInstance'd in the secondary thread.

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 a MTA class.

Conclusion

This is a short article with simple examples to demostrate the five scenarios. I used simple COM server and client applications because I don't want my readers to be distracted by needless complex examples from the information I want to convey in this article. Another reason is that 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 Singaporean and a COM expert whose STA articles have helped me a lot to understand COM STA objects and apartments.

References

History

  • 17th March, 2009, Fixed the problem of PeekMessage() taking 100% of the CPU time by using MsgWaitForMultipleObjects
  • 7th May, 2008, First release


About the Author

Wong Shao Voon

I guess I'll write here what I does in my free time, than to write an accolade of skills which I currently possess. I believe the things I does in my free time, say more about me.

When I am not working, I like to watch Japanese anime. I am also writing some movie script, hoping to see my own movie on the big screen one day.

I like to jog because it makes me feel good, having done something meaningful in the morning before the day starts.

I also writes articles for CodeGuru; I have a few ideas to write about but never get around writing because of hectic schedule.

Downloads

Comments

  • There are no comments yet. Be the first to comment!

Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • A help desk is critical to the operations of an IT services business. As a centralized intake location for technical issues, it allows for a responsive and timely solution to get clients and their staff back to business as usual. In addition to handling immediate IT issues, a help desk performs several proactive tasks to ensure clients' IT systems remain operational and downtime is minimized. Thus, utilizing a help desk and following best practices can improve the productivity, efficiency and satisfaction of …

  • Download the Information Governance Survey Benchmark Report to gain insights that can help you further establish business value in your Records and Information Management (RIM) program and across your entire organization. Discover how your peers in the industry are dealing with this evolving information lifecycle management environment and uncover key insights such as: 87% of organizations surveyed have a RIM program in place 8% measure compliance 64% cannot get employees to "let go" of information for …

Most Popular Programming Stories

More for Developers

Latest Developer Headlines

RSS Feeds