Give ActiveX-based Web Pages a Boost with the Apartment Threading Model
by David Platt
David Platt is president and founder of Rolling Thunder Computing (www.rollthunder.com). He is also the author of The Essence of OLE with ActiveX (Prentice Hall, 1996). David can be reached at dplatt@rollthunder.com.
You can significantly boost the performance of your ActiveX™ controls in multithreaded containers by making a few small modifications to your existing code. Why is this important? Because the first Web browser to support ActiveX controls, Microsoft® Internet Explorer 3.0 (IE 3.0), is a multithreaded container, and the forthcoming IE 4.0 (also known as Nashville) makes even more extensive use of threading. The performance profits that you can gain are not confined to these apps, but will apply to any multithreaded container that uses your controls.
This article will tell you how to make a significant performance gain without a whole lot of workfairly large bang, not too many bucks. As I take you through the process step by step, I will also explain how COM deals with threads, particularly as they apply to ActiveX controls. COM has a reputation for being hard to learn. This reputation stems primarily from the prevailing instruction model, which presents theory first, then examples long after your eyes glaze over. Ive written an entire book dedicated to the opposite approach (The Essence of OLE with ActiveX, Prentice-Hall), showing the simplest possible example and using it to explain the theory. Nobody Ive ever met, myself included, can stay awake for the theory without having an example to pin it on.
I strongly suggest that you download the sample code (see page 5 for instructions) and work along as you read this article. The sample programs are each in their own sample directory (provided you unzip the file with the d switch). Throughout this article I refer to sample directories; these are in the download file. The first sample control requires the MFC 4.0 DLLs, and the others require MFC 4.2.
Container Environment
Before examining controls in multithreaded containers, it would help to look at the container environment in which they will be running. Beginning with Windows NT¨ 3.51 and continuing in Windows¨ 95, COM has supported the use of objects on different threads in a manner dissected in detail later in this article. The container follows three simple rules and COM makes it all work.
First, every container thread that wants to use COM objects must call the function CoInitializeEx when it starts up and CoUninitialize when it shuts down. The first thread in the app that calls CoInitializeEx is called "COMs main thread" in that app. It doesnt have to be the apps main thread, the one on which WinMain was called, but for convenience it usually is. Unless I state otherwise, whenever I use the term "main thread" in the rest of this article, I am referring to COMs main thread. As the first thread to initialize COM, the main thread must also be the last thread to uninitialize COM.
Second, each thread that calls CoInitializeEx using the COINIT_APARTMENTTHREADED flag must have a message loop and service it frequently. (Note that calling the CoInitialize API is equivalent to calling CoInitializeEx with the COINIT_APARTMENTTHREADED flag.) COM uses private messages to hidden windows for its own internal purposes, as described later. If you fail to get and dispatch the messages that appear in a threads message queue, you will break COM. You probably dont have to change your code a bit. The normal message loop youve been using in your main thread has been doing exactly this since day one, and you probably havent heard a thing about COM and window messages. Theyve always been there; you just havent had any reason to notice. If your new threads are going to contain windows, youll have to do it anyway. Just make sure you dont plan a design with a thread that wants to use COM but doesnt check a message queue. (The message-loop requirement does not apply to multithreaded apartmentsthreads on which CoInitializeEx was called with the COINIT_MULTITHREADED flag.)
The final rule that the container must follow is that, when a thread obtains a pointer to a COM interface via any legal mechanism, that interfaces methods may only be called from that thread. For example, if a thread calls CoCreateInstance and obtains a pointer to an interface, that interfaces methods may only be called from within the thread that called CoCreateInstance. If this seems restrictive, fear not; there are legal workarounds described later in this article. But keep this rule in mind and youll have the right mental model.
If you follow the three simple rules I just discussed youll have a single-threaded apartment (STA) model container. Your threads are single-threaded apartment threads. That really reduces to following a few simple, consistent, easy rules. All of COM is like that, once you get to know it.
Nonthreaded Control
Suppose I have the simplest possible full-featured ActiveX control. I used the MFC 4.0 Control Wizard to generate a new ActiveX control project, accepting all the default options. I then added two lines to COleControl::OnDraw, using the API GetCurrentThreadId to get the ID of the thread on which this function was called, and outputting that ID in the controls window. This code is shown in Figure 1, and its about as simple as it gets. You will find the control and code in the sample directory \demo40. You will have to register the control demo40.ocx by using a command-line utility like regsvr32.exe.
Figure 1 A Simple Control
void CDemo40Ctrl::OnDraw(CDC* pdc, const CRect& rcBounds,
const CRect& rcInvalid)
{
pdc->FillRect(rcBounds, CBrush::FromHandle((HBRUSH) GetStockObject(WHITE_BRUSH)));
/*
Get ID of thread on which this function was called.
Draw in the control's window.
*/
char out [256] ;
wsprintf (out, "MFC V4.0\nThread ID == 0x%x",GetCurrentThreadId( )) ;
pdc->DrawText (out, -1, (LPRECT) &rcBounds, DT_CENTER) ;
}
I used the ActiveX Control Pad to place it on a page called demo40.htm. I then opened this page with IE 3.0. The control was created and shown in the IE 3.0 window. I then chose File New Window from the IE 3.0 main menu, bringing up another window showing the same page and containing another instance of my simple control. IE 3.0 does this by launching a new thread, not a whole new process, which you can verify by using PView. My screen then looked like Figure 2. The thread ID was the same for both controls, even though IE 3.0 created a new thread to handle the new page and this new thread called CreateControl internally. What is going on?
COM has not always supported multiple threads. It didnt in 16-bit Windows, obviously, and it didnt in Windows NT 3.5. In the latter, only one thread in a process was allowed to use COM and all COM function and object calls had to be made from this thread. If another thread tried to call CoInitialize, the call would fail.
Figure 2 MFC 4.0 ActiveX controls
With the release of Windows NT 3.51, COM added support for multiple threads. The approach used requires some cooperation from the objects server to keep controls on different threads from stepping on each others toes, but few control servers provided this cooperation at the time. To keep from breaking every control in existence, COM had to provide a mechanism for detecting whether or not a server knew how to handle itself in a multithreaded environment and for protecting the ones that couldnt while still allowing them to be used.
A control that has taken the necessary precautions to allow itself to be accessed by multiple threads identifies itself as such by making an entry in the registry (a process Ill describe later). When a container app calls CoCreateInstance to create a control, COM checks for the presence of this entry. In its absence, COM assumes that the controls DLL server has no knowledge of threads whatsoever. Controls built with MFC 4.1 or earlier, such as this example, fall into this category. If an object method that accessed global data was called on one thread, and another object method that accessed the same global data was called on another thread, the threads could conflict with each other and chaos would result.
For example, consider the code listing in Figure 3. Suppose I have a COM class called CSomeObject that has two member functions, AllocIfNeededAndUseMemory and FreeMemory. The first one checks to see if a pointer is NULL and, if so, allocates and initializes memory before using the pointer. The second one frees the memory and sets the pointer value back to NULL. The comment in the code points out the exact sequence of events needed for a conflict. While this example looks pretty silly, it was a reasonable way to write code back in the world of 16-bit COM and Windows NT 3.5, where you didnt have to think about preemptive multithreading.
Figure 3 A Possible Bug
char *pData = NULL ;
CSomeObject::AllocIfNeededAndUseMemory ( )
{
/*
If we are the first to use the data bank, allocate it and
initialize it.
*/
if (pData == NULL)
{
pData = malloc (LOTS_AND_LOTS) ;
Initialize (pData) ;
}
/*
Now use it for some purpose.
*/
DoSomethingWith (pData) ;
}
CSomeObject::FreeMemory( )
{
free (pData) ;
/*
A typical multi-threaded bug.
If we get swapped out at exactly this point, after freeing
the memory but before setting the pointer to NULL, then if
the top function got called on a different thread, it would
be accessing memory that had been freed.
*/
pData = NULL ;
}
To solve this problem, COM sets up marshaling code conceptually similar to that used for marshaling calls to another process, which ensures that all controls provided by that DLL are created on the containers main thread, the one that first called CoInitialize (or CoInitializeEx). This marshaling code also ensures that all calls to all methods of these controls are made only from the containers main thread. This way, all calls to the control are serialized in the manner that the control is expecting.
Since the thread handling the second window in the previous example wasnt the main thread, COM transparently marshaled the object creation into the main thread to avoid any possible conflict with other objects from the same DLL. The interface pointer returned to the second thread did not point directly to the code inside the object, as was the case in a single-thread situation. Instead, it pointed to a marshaling proxy object on the second thread. All of this happened within the second threads call to CoCreateInstance. When the second thread calls a method on this object, the proxy marshals the call to the main thread and marshals the results back to the second thread. This thread doesnt know or care whether it has a pointer to the actual object or to a marshaling proxy; it just calls the methods locally and the right thing happens. Each side follows the relatively simple COM rules, and COM intercedes as necessary to make it work transparently. The flow is shown in Figure 4.
While this may seem like an exotic and unusual thing for COM to do, it really isnt. Window message handling works the same way. A windows response function will always be called from the thread that created the window. Ever wonder why you dont have to serialize access to window message handlers? Its because the operating system marshals window messages the same way that COM does. In fact, COM accomplishes its marshaling by creating hidden windows and posting messages to them. You can see these windows in Spy++. Theyre the ones belonging to class OleMainThreadWndClass and having the text OleMainThreadWndName. For a full discussion of the blood and guts of multithreaded marshaling, see Don Boxs column in the April 1996 issue of MSJ.
Figure 4 A Nonthreaded Control in a Multithreaded Container
My container app can run with any control and vice versa, regardless of the threading used by either one, and I get my app out the door quickly. So whats the bad news? This marshaling takes time and burns CPU cycles. How bad a hit? Its tricky to measure; enough that you dont want it if you can avoid it. A ballpark figure is that marshaling a call from one thread into another takes about as much time as marshaling to another process. This means a call overhead of a few milliseconds each. Obviously, this can vary widely. Its worse in Windows 95 than in Windows NT. The more data you are passing, the longer it takes to copy, and if you are passing so much that the copy involves disk swapping, the delay can skyrocket. Worse, its not just the extra call overhead you have to deal with. The main thread to which the call is being marshaled might be busy doing a calculation or reading a long file, in which case the thread that originates the call will have to wait until the main thread finishes. Imagine five or six threads each creating ten controls, all needing all of their method calls marshaled back to the main thread. Youve lost the prime benefit of multithreading. Life becomes a weary burden.
Apartment Model-aware Control
Is there a way around this? Fortunately, yes, and its relatively easy. I generated and built the same control as the previous example, only this time using MFC 4.2, and put it on an HTML page called demo42.htm. You will find this page and the control demo42.ocx in the sample code directory \demo42. If you register the control, open the page with IE 3.0, and then open another IE 3.0 window showing the same page, you will find that the thread IDs are now different as shown in Figure 5. If you use Pview, you will find that the thread ID reported by the new control is the same as the new thread that IE 3.0 created to manage the new window. Unlike the previous example, the control has somehow been created on the new thread. What happened?
Figure 5 MFC 4.2 ActiveX control
Every server that provides an object to COM needs to specify how the objects functions may be called from multiple threads so that COM will know how to intercede between the object and its user. There are three choices. First, the server can refrain from specifying anything, as in the first example. In this case, COM sets up marshaling code to force all calls to all objects from that server to be made from a single thread (the main thread). Second, the server can specify that any of the objects functions may be called from any thread at any time; that the object is reentrant or internally synchronized to whatever extent it requires, so its perfectly safe for any thread to do anything to it at any time. This is the free threading modelalso called the multithreaded apartment (MTA) modelwhich is new in Windows NT 4.0 and is now available in Windows 95 with the DCOM for Windows 95 update. Because writing fully thread-safe code is complicated and not widely used, Ill deal with it at the end of this article.
The middle ground that COM has provided in Windows 95 and Windows NT (since version 3.51) is called the apartment threading model (more correctly called the STA model). A server that specifies its support for this model is promising COM that the server has serialized access to all of its global data, such as its object count. An apartment model object has not serialized access to the objects internal data, so if one thread calls a method on an object and gets swapped out in the middle, then another thread calls a method on the same object, the two calls could conflict. In the absence of the workaround described later in this article, COM requires you to call an objects methods only from the thread on which that object was created. Since this is the case, the object does not need to serialize access to its member variables, as these will always be called from the same thread. The objects do need to serialize access to the servers globals, as different objects may try to access these from different threads.
A control that supports this model is saying to COM, "Hey, Ive thought about threading, so Ive already taken care of serializing access to my own global data. You dont have to make all calls into me on a single thread the way you did with the previous idiot. Just make sure you call each of my objects on the thread that created it, and Ive made sure theyll never step on each others toes."
ActiveX controls created with MFC 4.2 or later support the single-threaded apartment model by default. A control that supports the single-threaded apartment model signals this to COM by making an entry in the registry as shown in Figure 6. The controls InProcServer32 registry key contains an additional named value, ThreadingModel. If this value contains the string data "Apartment," the control is promising that it supports the single-threaded apartment model. MFC has already taken care of the global object count for your control. If you are creating controls with MFC, then all you have to do is to serialize access to your globals as shown in the example coming up shortly.
Figure 6 Apartment registry entries
An MFC control registers itself as using the apartment model in the method COleControl::COleObjectFactory::UpdateRegistry, as shown in Figure 7. Control Wizard automatically generates this method for you. Its primary feature is a call to the function AfxOleRegisterControlClass, which makes the actual registry entries. The sixth parameter to this function is the constant afxRegApartmentThreading, which tells the function to register the control as supporting the apartment model. If you are writing a control with MFC version 4.2 or later and do not want to support apartment model threading, you must change this parameter to zero. Conversely, if you are upgrading a control that was originally generated with MFC version 4.1 or earlier and now want to support the apartment model, this parameter as generated in your original code will be FALSE and you must change it to afxRegApartmentThreading.
Figure 7 MFC 4.2 Registration of an Apartment Threaded Control
BOOL CDemo42Ctrl::CDemo42CtrlFactory::UpdateRegistry(BOOL bRegister)
{
// TODO: Verify that your control follows apartment-model
// threading rules. Refer to MFC TechNote 64 for more information.
// If your control does not conform to the apartment-model rules,
// then you must modify the code below, changing the 6th parameter
// from afxRegApartmentThreading to 0.
if (bRegister)
return AfxOleRegisterControlClass(
AfxGetInstanceHandle(),
m_clsid,
m_lpszProgID,
IDS_DEMO42,
IDB_DEMO42,
afxRegApartmentThreading,
_dwDemo42OleMisc,
_tlid,
_wVerMajor,
_wVerMinor);
else
return AfxOleUnregisterClass(m_clsid, m_lpszProgID);
}
Consider the code listing in Figure 3 that I used as an example in the previous section. It isnt hard to write code that makes sure that different threads never conflict over the same global variable. The code listing in Figure 8 is similar to Figure 3, but this time properly serialized. The quickest, easiest, and cheapest way of serializing access is with a critical section. You must write code to serialize access to any other globals that your control DLL contains. This also applies to class statics, which are really just a politically correct form of global.
Figure 8 An Apartment Threaded Control
CRITICAL_SECTION cs ;
DllMain (HANDLE hInst, DWORD dwReason, LPVOID pReserved)
{
if (dwReason == DLL_PROCESS_ATTACH)
{
InitializeCriticalSection (&cs) ;
}
}
char *pData = NULL ;
CSomeObject::AllocIfNeededAndUseMemory ( )
{
/*
Enter the critical section to make sure that no other thread can access the data pointer while we are using it.
*/
EnterCriticalSection (&cs) ;
/*
If we are the first to use the data bank, allocate it and
initialize it.
*/
if (pData == NULL)
{
pData = malloc (LOTS_AND_LOTS) ;
Initialize (pData) ;
}
/*
Now use it for some purpose.
*/
DoSomethingWith (pData) ;
/*
Release the critical section so any other thread that wants to can access the memory pointer.
*/
LeaveCriticalSection (&cs) ;
}
CSomeObject::FreeMemory( )
{
EnterCriticalSection (&cs) ;
free (pData) ;
/*
A typical multi-threaded bug.
If we get swapped out at exactly this point, after freeing
the memory but before setting the pointer to NULL, then if
the top function got called on a different thread, it would
be accessing memory that had been freed.
*/
pData = NULL ;
LeaveCriticalSection (&cs) ;
}
When a thread in the container calls CoCreateInstance to create an instance of control that supports the STA model, COM will create the object on the calling thread instead of marshaling everything over to the main thread as was done in the previous section. Since the creating thread has been initialized as STA, COM knows that it promised to call the objects functions only from the thread that created the object. COM therefore does not set up the marshaling code as it did in the previous example, so the interface pointer returned is a direct pointer to the objects code, as you would otherwise expect from an in-proc object. The two kids promise to play together nicely, so Mom leaves them unsupervised. This is the performance profit I referred before.
What if the creating thread wasnt initialized in the STA model? At the time of this writing, almost all of them are, and most of them will remain so for the foreseeable future. But again, you dont really have to care. Because of the controls registry entries, COM knows what it requires and will set up marshaling code if necessary so that your control sees what it expects. I deal with this issue towards the end of this article. The short answer is, dont worry about it; COM takes care of it.
Design Considerations
Your controls may want to create auxiliary threads to help them accomplish their work. If you do this, you should keep a few design guidelines in mind. In this section, I discuss the simple case of a generic background thread that does not access COM in any way. Then I will discuss legal ways in which this background thread can make calls on COM objects created by other threads.
Consider a simple control that creates a simple background thread. I wrote a control called BounceThread.ocx and placed it on an HTML page called bounce.htm, both of which you will find in the directory \BounceThread. If you register the former and open the latter with IE 3.0, you will see a window in which a bouncing ball ricochets around drawing a colored tail (see Figure 9).
Figure 9 BounceThread
To create an auxiliary thread in the MFC, I derived a class called CBounceThread from the MFC base class CWinThread. The code for creating the thread is shown in Figure 10. When the control receives its WM_CREATE message, it uses the function AfxBeginThread to create an object of this class. Its created in the suspended state so the code can initialize its member variables before it goes charging off, bouncing its ball around the controls window. After this, it calls ResumeThread and goes off on its merry way.
Figure 10 Creating the Bounce Thread
int CBounceThreadCtrl::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
if (COleControl::OnCreate(lpCreateStruct) == -1)
return -1;
/*
Create new thread in suspended state.
*/
m_pThread = (CBounceThread *)AfxBeginThread (
RUNTIME_CLASS(CBounceThread),
THREAD_PRIORITY_LOWEST,
0,
CREATE_SUSPENDED) ;
/*
Set the threads member variables, in this case, the control that created it.
*/
m_pThread->m_pControl = this ;
/*
Resume the thread, allowing it to run
*/
m_pThread->ResumeThread( ) ;
return 0;
}
Creating the thread is easy; destroying it is trickier. When you get tired of the thread, you cant just blow it away via the function TerminateThread. This leads to all kinds of nasty consequences, such as not deallocating the threads stack, not releasing mutexes owned by the thread, and not notifying any DLLs of the thread terminationjust not a good thing to do at all. It might just be OK if you knew that your entire app was going down, but that isnt the case in an ActiveX control. The control could very easily be destroyed because the user surfed to another page. You have to find another approach.
To get rid of a thread, it is a much better idea to have its cooperation. The MFC base class CWinThread already provides this in its handling of the WM_QUIT message. When this message appears in the message queue of a CWinThread, it nicely exits its message loop, calls its ExitInstance method, cleans up after itself, and quietly vanishes. In the sample control, this is done in the controls WM_DESTROY message handler, as shown in Figure 11. After posting this message, the threads priority is raised to help it finish up whatever it has to do before it dies. The control is probably being destroyed as a result of a direct command from the user, so this is a good use of a high priority. Since the thread accesses the controls member variables in its processing, notably the controls m_hWnd for getting a DC, I dont want the control to disappear before the thread. I therefore use the API function WaitForSingleObject to wait for the thread to die, finish my controls cleanup, and leave.
Figure 11 Destroying the Bounce Thread
void CBounceThreadCtrl::OnDestroy()
{
COleControl::OnDestroy();
/*
Post thread a WM_QUIT message telling it to nicely commit suicide.
*/
m_pThread->PostThreadMessage(WM_QUIT, 0, 0) ;
/*
Boost the threads priority so it can finish whatever it has to do before it dies.
*/
m_pThread->SetThreadPriority(THREAD_PRIORITY_TIME_CRITICAL) ;
/*
Wait until the thread has in fact died. Closing the threads handle is done in the CWinThread base classs destructor.
*/
WaitForSingleObject (m_pThread->m_hThread, INFINITE) ;
}
The thread needs to make sure that it keeps checking its message queue so it responds to the WM_QUIT message when one arrives. If it needs to wait on a synchronization object, the thread should do so via the function MsgWaitForMultipleObjects so that the wait will return if a mes-sage comes into the threads message queue. If you are not using the CWinThread MFC base class, you will have to write your own mechanism for signaling the thread to shut itself down.
The other major design consideration is to make sure that the auxiliary thread doesnt have such a high priority that it starves other threads of needed CPU cycles. If you right-click on the control in IE 3.0, you will find that Ive put a context menu on it allowing you to experiment with different priority levels. Anything higher than THREAD_PRIORITY_NORMAL will make the user interface very unresponsive. Dont try this with unsaved data in any of your other apps.
Marshaling Interface Pointers
Nowhere are the intricacies of multithreaded COM objects more delicate than in the interaction between an ActiveX control and its container. In a control-container situation, the distinction between client and server becomes meaningless. It is customary to think of a control as a server and its container as a client. A full-featured control provides at least seven interface pointers that the container may call. However, the controls container provides at least six interface pointers that the control can call and frequently does. So each side is the client of a handful of interfaces and the server of another handful.
Suppose I have a control that performs an operation that could take a long time to complete, for example, searching a large or slow database. I would like my control to have a method that starts the operation and returns immediately, rather than hanging the calling thread by not returning until the long operation completes. When the operation finally does complete, I want it to signal its completion asynchronously via an event.
An easy way to do this is for the control to create an auxiliary thread that performs the search operation, and for this thread to signal the completion of its search by firing an event. It seems simple enough, but theres a hidden problem. Events are signaled by calling the Invoke method of an IDispatch interface supplied by the container. This interface lives in the apartment of its own thread, which is also the one containing the control itself. To comply with the STA model, I can only safely call this interfaces methods from the thread in which it was created. If the control spins off another thread, when the new thread wants to signal back, it will be calling an object pointer on a thread other than the one that created it. Doing this would violate the rules of the apartment threading model.
Fortunately, as usual, theres a legal workaround that isnt too painful. Look back at the first simple example where COM set up marshaling code to make sure that an objects methods were only called on the thread from which the object was expecting it. In that case, all object calls were marshaled to the main thread. I can set up marshaling code myself, which will allow the containers event IDispatch to be called from the database searching thread. When I do this, the call will be marshaled from the search thread into the controls thread, from which it can be legally made to the container, and the result marshaled back to the search thread. This is the same thing that COM did in the first simple example, just to and from different threads.
Consider the code in the directory sample \ThreadDB, which contains a control called threaddb.ocx and an HTML page called threaddb.htm. If you register the former and open the latter with IE 3.0, you will see the screen shown in Figure 12. The idea is to simulate a search program that takes a Social Security number and returns an IDispatch object representing a customer, having such properties as first and last name. The VBScript listing is shown in Figure 13. You enter a Social Security number and click Start Search, thereby calling that method on the ThreadDB control. For simplicity, I didnt bother reading the Social Security number out of the edit control; I just arbitrarily pass a value of 444. When the operation completes (very quickly in this example), it fires back an event called SearchFinished. This event passes as a parameter an automation object of class CustomerObj, from which VBScript fetches the properties that represent the customers name and address and places them in the edit controls on the page. The VBScript listing actually lives in the file threaddb.alx, a layout control that I built with ActiveX Control Pad to get the layout to look nice. Enter anything or nothing into the Social Security number box, click Start Search, and presto, that number has identified a client named John Smith.
Figure 12 ThreadDB sampleDB
<SCRIPT LANGUAGE="VBScript">
<!--
rem Variable to be store customer object
dim foo
rem This function called when "Start Search" button clicked.
rem Call StartSearch method on ThreadDB control
Sub CommandButton1_Click()
ThreadDB1.StartSearch (444)
end sub
rem This function called when ThreadDB control fires SearchFinished
rem event. Set customer's first and last names into edit controls.
rem Save Customer object for future use in demo.
Sub ThreadDB1_SearchFinished(CustomerObj)
set foo = CustomerObj
TextBox2.Text = CustomerObj.LastName
TextBox3.Text = CustomerObj.FirstName
end sub
rem This function called "Refresh" button clicked. Refresh
rem customer's names from stored object
Sub CommandButton2_Click()
TextBox2.Text = foo.LastName
TextBox3.Text = foo.FirstName
end sub
-->
</SCRIPT>
The control exposes a single method called StartSearch (see Figure 14), which VBScript calls when you click the Start Search button. I first use the function AfxBeginThread to create a new thread, which pretends to perform a database search. The thread belongs to the class CSearchThread, defined in Figure 15. As before, I create the thread in a suspended state so I can initialize its member variables before it goes charging off into the database. In addition to its normal state variables, such as the Social Security number of the customer I want to search for, I also need to give it the IDispatch interface pointer that I want it to use to signal its completion. The former task is trivial. The latter is trickier.
Figure 14 StartSearch Method of ThreadDB
BOOL CThreadDBCtrl::StartSearch(long SocialSecurityNumber)
{
/*
Create thread object in suspended state. Set member variables.
*/
m_pSearchThread = (CSearchThread *) AfxBeginThread (
RUNTIME_CLASS (CSearchThread),
THREAD_PRIORITY_NORMAL, 0,
CREATE_SUSPENDED) ;
m_pSearchThread->m_pCtrl = this ;
m_pSearchThread->m_ssn = SocialSecurityNumber ;
/*
Find all the event IDispatch interface pointers and marshal them to the thread.
*/
IDispatch * pDispatch; IStream * pStream ; HRESULT hr ;
POSITION pos = m_xEventConnPt.GetStartPosition();
while (pos != NULL)
{
/*
Get next IDispatch in connection point's list.
*/
pDispatch = (LPDISPATCH)m_xEventConnPt.
GetNextConnection(pos);
/*
Create stream for marshaling IDispatch to search thread.
*/
hr = CoMarshalInterThreadInterfaceInStream(
IID_IDispatch, // interface ID to marshal
pDispatch, // ptr to interface to marshal
&pStream) ; // output variable
if (hr != S_OK)
{
AfxMessageBox ("Couldn't marshal ptr to thread") ;
}
/*
Place stream in member variable where search thread will look for it.
*/
m_pSearchThread->m_StreamArray.Add (pStream) ;
}
/*
Allow thread to resume running now that we have initialized
all of its member variables.
*/
m_pSearchThread->ResumeThread ( ) ;
return TRUE;
}
Marshaling an interface to another thread is accomplished via two API functions. The thread that owns the legal interface pointer calls CoMarshalInterThreadInterfaceInStream, which creates a memory structure containing all the information that COM needs to set up marshaling into the owning thread. This function provides that information in the form of a Structured Storage stream, represented by an IStream interface pointer. Once you have this stream, you must somehow get it to the destination thread that wants to call the interface owned by the original thread. The destination thread calls the API function CoGetInterfaceAndReleaseStream, passing a pointer to the stream containing the marshaling information. This function returns a pointer to the original interface that the new thread may now call. This is a proxy that actually marshals the call into the original thread rather than a direct connection.
Figure 15 CSearchThread
class CSearchThread : public CWinThread
{
DECLARE_DYNCREATE(CSearchThread)
protected:
CSearchThread(); // protected constructor used by dynamic
// creation
// Attributes
public:
CPtrArray m_DispatchArray ;
CPtrArray m_StreamArray ;
CThreadDBCtrl *m_pCtrl ;
long m_ssn ;
// Operations
public:
// Overrides
// ClassWizard generated virtual function overrides
//{{AFX_VIRTUAL(CSearchThread)
public:
virtual BOOL InitInstance();
virtual int ExitInstance();
//}}AFX_VIRTUAL
// Implementation
protected:
virtual ~CSearchThread();
// Generated message map functions
//{{AFX_MSG(CSearchThread)
//}}AFX_MSG
DECLARE_MESSAGE_MAP()
};
To find the IDispatch pointers used for signaling a controls events to its container, I traced down into the MFC source code to the function COleControl::FireEventV, the base function responsible for firing all events. When it fires an event, it uses the IDispatch pointers that it has stored in a member variable called COleControl::m_xEventConnPt. It shows that more than one container IDispatch might want to be notified of an event. I lifted the relevant portions of the code and bingo, there was a list of all IDispatch pointers that are listening for events from this control.
In the sample program, StartSearch iterates over all the IDispatch pointers from the container and calls CoMarshalInterThreadInterfaceInStream for each of them, receiving an IStream interface pointer in return. Ive never seen there be more than one, but it's better to be safe than sorry. The CSearchThread class contains a member variable called m_StreamArray, which represents an array of pointers to IStream interfaces. The control places each of the IStream interface pointers into this array.
The thread that performs the simulated search is an object of class CSearchThread. When the CSearchThread starts up, the MFC framework calls the threads InitInstance method, as shown in Figure 16. Since the new thread wants to access COM, it must first call CoInitialize. It then wants to get the interface pointers to the IDispatch interfaces that it will use to signal completion of its search. The CSearchThread contains the aforementioned array of streams containing the information it needs to create these. It iterates over the array of IStream interface pointers provided by its creator, calling CoGetInterfaceAndReleaseStream to convert each into an IDispatch pointer. As previously stated, these pointers are proxies, not direct connections.
I then create a CCustomer automation object with the name of John Smith to simulate the result of the search. This is an automation object that I used Class Wizard to derive from CCmdTarget. It contains the properties of FirstName and LastName. Finally, I use each IDispatch pointer to fire the SearchFinished event to each event sink that might be waiting, using the method COleDispatchDriver::InvokeHelper. I lifted the code straight from COleControl::FireEventV.
One thing looks strange here. The CCustomer automation object that the search thread provides as a parameter to the event is a COM object, but I dont seem to be marshaling it even though its properties are being accessed by IE 3.0 on another thread. Have I omitted something, or is this legal? Its legal. COM knows how to marshal method parameters. Because the parameter being passed to the event function is an IDispatch interface pointer and identified as such by the flags in the VARIANT structure that contains it, COM knows what type of marshaling is needed and sets it up automatically. The event function in VBScript actually receives not a direct connection to the CCustomer automation object, but a proxy that is valid in VBScripts thread. When VBScript calls the Invoke method to get the FirstName and LastName properties, the call is marshaled to the search thread and the result marshaled back to VBScript. Thats one of the beauties of COMthe dirty details are hidden away. You just follow a few simple and consistent rules for negotiating with COM and everything works more or less the way you think it ought to.
Figure 16 InitInstance and ExitInstance methods of CSearchThread
BOOL CSearchThread::InitInstance()
{
HRESULT hr ; int i ;
/*
Initialize COM in this thread.
*/
hr = CoInitialize (NULL) ;
/*
Unmarshal interface pointers
*/
IDispatch *pDispatch ; IStream *pStream ;
for (i = 0 ; i < m_StreamArray.GetSize( ) ; i ++)
{
/*
Get stream pointer from array where control has placed it.
*/
pStream = (IStream *) m_StreamArray [i] ;
/*
Use stream pointer to create IDispatch the I can call from
this thread.
*/
hr = CoGetInterfaceAndReleaseStream(
pStream , // stream containing marshaling info
IID_IDispatch, // interface desired
(void **) &pDispatch) ; // output variable
if (hr != S_OK)
{
AfxMessageBox ("Couldn't get interface") ;
}
/*
Put resulting IDispatch in my array of those pointers.
*/
m_DispatchArray.Add (pDispatch) ;
}
/*
Create new client object with arbitrary name
*/
CCustomer *pCustomer = new CCustomer ("John", "Smith", 999) ;
LPDISPATCH pDispOut = pCustomer->GetIDispatch (FALSE) ;
/*
Fire SearchFinished event to container
*/
COleDispatchDriver driver;
for (i = 0 ; i < m_DispatchArray.GetSize( ) ; i ++)
{
driver.AttachDispatch((IDispatch *)m_DispatchArray[i], FALSE);
driver.InvokeHelperV(
m_pCtrl->eventidSearchFinished,
DISPATCH_METHOD, VT_EMPTY, NULL,
EVENT_PARAM(VTS_DISPATCH),
(char *)&pDispOut);
driver.DetachDispatch();
}
/*
Release customer object. If event handler wanted a pointer to it,
it will have AddRefd it.
*/
pCustomer->GetIDispatch (FALSE)->Release ( ) ;
return TRUE;
}
/*
PostQuitMessage( ) is called from CCustomer::OnFinalRelease( ), thereby
ensuring that the thread stays around long enough to service any
customer objects that it created. When that happens, this function is called. Uninitialize COM.
*/
int CSearchThread::ExitInstance()
{
CoUninitialize( ) ;
return CWinThread::ExitInstance();
}
If calls on the CCustomer proxy are marshaled back to the thread that created it, does this mean that the thread has to stay around as long as the object does? Yes, it does. To prove that to yourself, rebuild the control, adding a call to PostQuitMessage at the end of the CSearchThread::InitInstance method, thereby causing the thread to terminate as soon as it returns from signaling the event. When you click Start Search, John Smiths name appears in the edit controls because the code that fills the controls is executed during the event, while the thread is still alive. If you then click the Refresh button, attempting to access the CCustomer automation object again, you will see an error box from VBScript announcing an error of type 0x80010012L. If you look that up in the header files, you will find this constant defined as RPC_E_SERVER_DIED_DNE. You cant make a call into an object if the thread that created the object isnt around to service the call.
To solve this problem in the sample control, I overrode the OnFinalRelease method of my CCustomer object. This is called when the last user of the object calls Release and the objects reference count drops to zero. In it, I call PostQuitMessage, thereby sending the WM_QUIT message to the search thread. In the threads ExitInstance method, I uninitialize COM via the function CoUninitialize.
How do I know if the interface really needed to be marshaled at all? It might have been a free-threaded interface that wouldnt care if it was called from another thread. I dont know, but fortunately I dont have to know or care. If I simply make the function calls to marshal the interface as shown above, and the interface actually belongs to an object that doesnt require marshaling, COM will simply not set up the marshaling code behind the scenes. Instead of a proxy, CoGetInterfaceAndReleaseStream will return a direct pointer to the objects code. I follow a few simple rules and COM intercedes to the extent necessary. I dont have to know what that extent is in order to use it, and Id just as soon not have to think about it.
Non-MFC Single-Apartment Threaded Controls
The previous examples have focused on controls implemented with MFC and its base class COleControl, which is where most full-featured controls have historically come from. Todays rush to the Internet is leading developers to such tools as the ActiveX Template Library and the SDK CBaseCtrl class to keep the size of their code to a minimum. What design considerations are important if you are rolling your own apartment-threaded control instead of using MFC? Most of it turns out to be fairly simple, but theres one thorny problem, which Ill describe after I show you the easy stuff.
You have to place the "Threading Model" data entry into the registry yourself, and thats simple. All of your objects have to serialize their access to your control DLLs global data, as was done in the previous examples. Thats also relatively simple, and is the same as for an MFC-based control anyway.
You now have to ensure proper handling of the named global functions that your DLL exports to COM, specifically DllGetClassObject and DllCanUnloadNow. Any thread in the container that calls CoCreateInstance, CoGetClassObject, or their kin will eventually wind up in the former function, so you have to make it thread-safe. This, too, is fairly simple. You can either write a single class factory and make that reentrant, or you can create a new class factory every time DllGetClassObject is called. The example shown in Figure 17 uses the latter.
Figure 17 DllGetClassObject and DllCanUnloadNow
static const GUID GUID_MyGuid = { 0xe7767a70, 0xa409, 0x11ce,
{ 0xad, 0xf, 0x0, 0x60, 0x8c, 0x86, 0xb8, 0x9c } };
LONG g_ObjectCount = 0 ;
STDAPI DllGetClassObject(REFCLSID rclsid, REFIID riid, LPVOID FAR* ppv)
{
/*
Check to make sure that COM is asking for the CLSID that we support.
Maybe someone got the registry entry wrong.
*/
if (rclsid != GUID_MyGuid)
{
return E_FAIL ;
}
/*
Create a new class factory object and call QueryInterface on it, on
behalf of the COM that called this function.
*/
CClassFactory *pCF = new CClassFactory ( ) ;
HRESULT hr = pCF->QueryInterface (riid, ppv) ;
if (hr != S_OK)
{
delete (pCF) ;
}
return hr ;
}
STDAPI DllCanUnloadNow(void)
{
if (g_ObjectCount == 0)
{
return S_OK ;
}
else
{
return S_FALSE ;
}
}
The function DllCanUnloadNow is where the problem lies. This function gets called when the container calls CoFreeUnusedLibraries. COM is asking each server DLL whether it has objects outstanding and therefore cannot be unloaded. Your function returns S_OK if it can be unloaded because it does not have any objects currently outstanding, or S_FALSE if it cannot be unloaded because it does. Your DLL needs to maintain a global object count to answer this question properly, which is simple enough. You have a global variable as shown in Figure 17. Every time your DLL creates an object, you increment it, and every time an object is destroyed, you decrement it. Make sure you use the thread-safe functions InterlockedIncrement and InterlockedDecrement for manipulating this count, as shown in the implementation of the class factory in Figure 18. The simple C++ statement
g_ObjCount++ ;
is not thread-safe. Look at the assembler listing of it if you dont believe me.
Consider the listing of DllCanUnloadNow shown in Figure 17. Suppose the thread that called the function got swapped out just before the return statement and another thread got swapped in and called CoGetClassObject (which calls DllGetClassObject) to get a class factory. Youve already checked the object count and decided it was zero. When DllCanUnloadNow eventually gets swapped back in and returns S_OK, you would think that the server DLL would be unloaded and the other threads call to the class factory would reference code that wasnt there any more. Still no problem; COM serializes the calls to these two functions so that one has to return before the other can be called. In this situation, COM will block the thread calling DllGetClassObject until the call to DllCanUnloadNow returns.
So lets look at the thorny problem. Consider the class factory destructor shown in Figure 18. Suppose this was the last object provided by my server and it had just been released by the thread that owned it. The destructors code calls InterlockedDecrement, the global object count goes to zero, but the return statement hasnt been executed yet. Now suppose another thread gets swapped in and calls CoFreeUnusedLibraries, causing DllCanUnloadNow to be called. This function checks the global object count, finds that its zero, returns S_OK, and the DLL gets unloaded. The first thread gets swapped back in, tries to execute the return statement, but the code isnt there anymore because the DLL got unloaded. Boom, you crash.
Figure 18 CClassFactory
/*
Increment global object count at our creation. Decrement at our destruction. The object manufactured by this factory do the same thing in their constructors and destructors.
*/
CClassFactory::CClassFactory (void)
{
InterlockedIncrement (&g_ObjectCount) ;
}
CClassFactory::~CClassFactory(void)
{
InterlockedDecrement (&g_ObjectCount) ;
}
STDMETHODIMP CClassFactory::CreateInstance(LPUNKNOWN pUnkOuter,
REFIID riid, LPVOID* ppvObj)
{
/*
Check for aggregation request. We don't do it.
*/
if (pUnkOuter != NULL)
{
*ppvObj = NULL ;
return CLASS_E_NOAGGREGATION ;
}
/*
Create a new data object. Query interface as requested.
*/
CMyObj * pObj = new CMyObj ( ) ;
HRESULT hr = pObj->QueryInterface (riid, ppvObj) ;
if (hr != S_OK)
{
delete pObj ;
}
return hr ;
}
Its difficult for a control to protect itself against this without being badly behaved. Don Box, in his January 1997 column, proposed two solutions. DllCanUnloadNow can always return S_FALSE, so there will never be any conflict, but thats not a nice thing. Or you could set a timer in the objects destructor, and change DllCanUnloadNow to require that both the object count be zero and the timer expire before returning S_OK. This assumes that whatever timeout interval you selected would give the destructor the chance to finish returning. Neither option is fantastic. In practice, the control cannot definitively protect itself against this situation without violating other rules. It is up to the container to pay attention and only call CoFreeUnusedLibraries in idle moments, when it isnt creating or destroying objects.
The Multithreaded Apartment Model
What about the MTA model (also called the free threading model), which has gotten so much press these last six months or so? A full discussion of it would require another article of this length or more. Ill try to give you an idea in the next few paragraphs of what it means for controls and their containers.
A control that supports the MTA model is saying to COM that it has already internally serialized all of its methods to whatever extent they require, and therefore any method on any of its objects can be called from any thread at any time. Like most things, this approach has advantages and drawbacks. MTA controls have the potential to run faster when being called by threads other than the ones on which they were created. STA controls are easier to write.
Each thread in an app may identify itself as an STA thread or an MTA thread. Single-threaded apartment is the default provided by CoInitialize. A thread that wants to identify itself as MTA must initialize COM by calling the new function CoInitializeEx using the COINIT_MULTITHREADED flag (this flag was not supported prior to the DCOM update for Windows 95). You can mix and match single and multithreaded threads in the same process.
Based on the value of the ThreadingModel registry entry, a control can identify itself as nonthreaded (None or absent), single-threaded (Apartment), multithreaded (Free), or able to provide direct connections in either STA or MTA models (Both). You can use any of them anywhere and COM will intercede to the extent needed to make it work. Its not a matter of getting it to work at all, but of getting it to work optimally.
If a single-threaded apartment thread creates an instance of a control marked only as Free, COM will marshal all calls into or out of that control. In the absence of the Both key, the MTA control has not promised to make all callbacks into the container (for example, firing events), only on the thread that provided the interface, which the single-threaded apartment model requires. The performance advantages in IE 3.0 and IE 4.0 obtained by a direct connection apply only to controls that support STA or both models, not to those controls that support the MTA model alone.
If a multithreaded apartment thread creates a control marked as Free, COM will not set up marshaling. Methods on these controls may be freely called by any thread in the process without marshaling. An MTA-aware control has promised COM that it contains whatever serialization code it requires. Its saying, "Go ahead, do anything to me from anywhere. Im tough! I can take it!"
If an MTA thread creates an STA control, COM will set up a marshaling proxy. The interface pointer can be called from anywhere, but the call will be marshaled back into the thread that originally created the control because COM knows thats what an STA control requires. The one place where you make a performance profit with the MTA model that isnt available in the STA model is when you have an MTA control in an MTA thread and call the controls methods from another thread. In the STA model, the call from the second thread would have to be marshaled, with attendant performance penalties. In the MTA model, it proceeds directly.
Which model should your control support? It basically depends on who your customers are going to be. If youre writing controls that you intend to place on HTML pages for the use of browsers, these are multithreaded apps that always initialize their threads as STA, and will continue to be for the foreseeable future. The MTA model wont buy you anything here because the container doesnt know how to take advantage of it. Multithreaded apartments are used most in DCOM scenarios. If your components will be serving incoming DCOM calls, you can achieve significant performance and scalability by making them fully thread-safe and marking them with Both.
If your control supports the STA model, COM provides you with object-level serialization for no development effort on your part, and you will still have a direct connection to STA threads. If most of your objects methods need this serialization, you might as well write an STA control and get it out the door quickly and bug free. For example, if your control is doing lots of user interface programming, such as in-place activation required of full-featured controls, you would have to write code to serialize most of your controls methods anyway, so why not take what COM gives you?
The place where the fully thread-safe model makes you a profit is if your control has methods that dont need very much serialization and if the container wants to access the same control from multiple MTA threads. Consider the IDataObject interface, an interface so simple that I use it for the very first COM example in my book. It contains nine member functions in addition to IUnknown. Most of this interfaces methods are simple enough that they dont require any type of serialization; they can be made completely reentrant. For a one-way data transfer object of the type used for dragging and dropping text from one window to another, six of them are usually stubbed out, simply returning a hardwired error code saying, in effect, "No, I dont do this." Theres no reason that these couldnt be called from any thread at any time, thereby saving the effort of marshaling these calls. If the data object represents a static snapshot, as is usually the case, the other three methods dont need serialization either. If they did, you could write it yourself, taking the performance hit on only three methods instead of all nine. Of course, this only works if the container supports MTA controls.
To summarize, full-featured controls to be used by browsers want the STA model. Fortunately, thats the easier of the two to write. If you are going to write an MTA control, consider supporting both threading models, or youll take a performance hit in an STA-threaded container.
Miscellaneous Gotchas!
Here, in no particular order, are miscellaneous tips from the trenches in writing STA controls. It probably wont make much sense the first time, but after youve worked with this stuff for a while itll start to gel.
When you are testing your controls, make sure that you do so with several different pages, each containing several instances of your control. The interaction effects tend to bring to the surface bugs that are not easily found by other methods. Use the debugging versions of MFC and the operating system to check for leaks, which is the favorite kind of bug for a control.
If your control is exposing any kind of custom interface, you will need to supply a marshaler for it. You are used to thinking of in-proc interfaces as not requiring marshaling, and in the case of a single-threaded container this is true. Once you are in a multithreaded container, interface pointers need to get marshaled between threads all the time, and this wont happen without a marshaler. All of the standard interfaces already have these marshalers in the operating system, but your custom interfaces will not. Writing an interface marshaler is quite easy using MIDL.
In an STA control, maintaining per-object storage is trivial as the member variables of your control are thread-safe in this model. Access to global variables (per-DLL storage) needs to be serialized, but thats not a big problem. A somewhat more intricate problem is knowing when to allocate and release objects that you have one of per thread. For example, since GDI does not serialize access to its objects such as fonts or brushes, it is sometimes advantageous to keep one of these per thread. Or perhaps you created a hidden window (the ultimate thread-centric object) to use for communication between your controls. You could keep one per object, but that would be wasteful, particularly in Windows 95. "No problem," you say, "Ill just use the DLL_THREAD_ATTACH and DLL_THREAD_ DETACH notifications that I get in my control DLL." Not really. You dont get these when you need them.
Suppose you have the IE 3.0 main window showing a page that doesnt contain your control. You open another IE 3.0 window and surf to a page that does contain the control. Your controls DLL is loaded, you get the DLL_PROCESS_ATTACH notification (which implies the first thread notification as well), and you allocate a brush for that thread. No problem. Now suppose the user goes back to the first window and opens another page containing your control. Another instance of your control will be created on the first windows thread, but you wont get a DLL_PROCESS_ATTACH notification because the thread already existed prior to your controls DLL being loaded.
The place to put your new thread-detection code is in DllGetClassObject. Any thread that wants to create an object will have to go there to get the class factory pointer. You can call GetCurrentThreadId to find the thread on which you are being called and go from there.
You have a similar problem in knowing when to release your per-thread data. You might have all your controls on a thread be destroyed without the thread going downthe user just surfs to another page. So each of your objects that allocate per-thread data will have to check in its destructor to see if it was the last survivor on that thread, and release the per-thread data if so.
Conclusion
You can make your ActiveX controls run a whole lot faster in multithreaded containers such as IE 3.0 and IE 4.0 if you use the single-threaded apartment model. Its not hard to do this; simply rebuild under MFC 4.2 or later and guard access to your global data variables with critical sections. Remember, in the single-threaded apartment model you may only call an objects method from the thread on which the object was created unless you do some serious negotiation with COM.
If you want to find out more about the threading models available in COM, the best place to start is with the Microsoft KnowledgeBase article, "Descriptions and Workings of OLE Threading Models," number Q150777.
To obtain complete source code listings, see Editor's page.
This article is reproduced from Microsoft Systems Journal. Copyright © 1997 by Miller Freeman, Inc. All rights are reserved. No part of this article may be reproduced in any fashion (except in brief quotations used in critical articles and reviews) without the prior consent of Miller Freeman.
To contact Miller Freeman regarding subscription information, call (800) 666-1084 in the U.S. and Canada, or (303) 678-0439 in all other countries. For other inquiries, call (415) 905-2200.