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);
}

More by Author

Must Read