I've never done a user-interface thread that has windows or controls. On the other hand, the complement of a worker thread is not necessarily a thread that has user-interface objects. The complement of a worker thread is a thread that has a message pump. This is a subtle but important distinction, but one that I have either needed or exploited, depending on circumstances.
I don't pretend to be an expert on "apartment threading". I fell into it because I needed to know a small number of things about it, specifically, how to get an SAPI-enabled application to run.
The key to "Apartment threading" is that an object which is initialized should be initialized in a particular thread and all operations on that object must be performed from that same thread. Trying to do operations on the same object from other threads is not guaranteed to work correctly. The point of this being that the object can be then written without worrying about incorporating thread-safe operations, because only one thread ever accesses it.
The Message Pump
What I needed to run SAPI in a separate thread was not a worker thread, but a thread with a message pump. The thread does not have to have GUI objects, and hence the name "User-Interface Thread" is a serious misnomer. The user interface may not even be relevant.
What SAPI requires is a message pump. So I had to start a "user-interface" thread. There were a couple problems, so this essay tries to capture my experience.
Creating a User-Interface Thread
First, there is the question about how to create the thread. What you first need is a
CWinThread-derived class, which is then necessarily a
CCmdTarget-derived object. Use the ClassWizard to create it, and you will get all the correct declarations. For purposes of this essay, it will be called
Now, you would expect to use
AfxBeginThread, in the approved fashion, doing
CMyThread * thread = AfxBeginThread(RUNTIME_CLASS(CMyThread));
Well, it isn't quite that easy. It should be, but it isn't. What's missing here is a way to pass initial parameters in to the thread!
Consider a couple cases: you don't have a
CMyThread object until the
AfxBeginThread completes. But by the time you get control, the thread may already be running, and therefore it may be too late to set any values.
There are a couple solutions to this. One is to create the thread suspended, by providing the
CREATE_SUSPENDED flag. Note that because this does not default we have to provide the intermediate parameters, the thread priority and the call stack size, so provide the values which are the defaults:
CMyThread * thread = AfxBeginThread(RUNTIME_CLASS(CMyThread),
And upon completion, you can then set any member variables in the class, and explicitly resume the thread:
thread->m_Whatever = ...;
thread->m_OtherThing = ...;
I chose to do the two-step method suggested in the
CMyThread * thread = new CMyThread();
thread->m_Whatever = ...;
thread->m_OtherThing = ...;
I did this just because I did not like to explicitly specify the defaults.
The Thread Function
OK, you've already done worker threads. You know that you provide a function that is called to execute the thread body, and the thread terminates when this function terminates, the thread ends. But what about a user-interface thread? Where is your function?
The answer is that it is
CWinThread::Run is the thread function. This is the message pump. When it exits, your thread terminates.
Using the Thread Function
The key methods you might be interested in are virtual methods of your class. You can create these using ClassWizard:
BOOL PreTranlsateMessage(LPMSG msg) allows you to process messages before letting them be handled by the usual message dispatch. If you process the message at this point and do not want it dispatched, you must return
TRUE. If you have processed it and want normal processing to continue for it, or you don't want to process it yourself, you return
BOOL OnIdle(LONG count) is called as long as you have "idle tasks" to do and there are no other messages. The first time it is called for idle processing the count is 1, and the count is incremented on each subsequent call. If you return
TRUE you will be called again, and again, until you return
FALSE. Any time there is a new message to be processed, the counter is reset to 0. If you return
FALSE, the message loop blocks until a message comes in.
BOOL InitInstance() is where you can do any setup for your class. For example, this is where I call
CoInitialize(NULL) to initialize the COM subsystem to support the SAPI objects I'm calling. Note that
CoInitialize is thread-specific and must be called for each thread that is calling ActiveX objects. If
InitInstance fails, you should return
FALSE. This will cause the thread to terminate.
void ExitInstance() is where you do any cleanup for your class. It is called after the message pump terminates. This is where I do
CoUninitialize() to clean up the COM resources allocated for the thread.
BOOL PostThreadMessage(UINT msg, WPARAM wParam, LPARAM lParam) allows you to post a message to the thread. It returns
TRUE if successful and
FALSE if error.
Using the Message Map
The Message Map for a
CWinThread does not support all messages, although it won't complain if you try to put something unsuitable in it. You need to use the macros
ON_THREAD_MESSAGE(UINT msg, LRESULT (handler *)(WPARAM, LPARAM))
ON_REGISTERED_THREAD_MESSAGE(UINT msg, LRESULT (handler *)(WPARAM, LPARAM))
ON_THREAD_MESSAGE macro requires that the msg parameter be a constant, typically a
(WM_APP+n) value. The
ON_REGISTERED_THREAD_MESSAGE macro requires that the msg parameter be the name of a
UINT variable that holds the Registered Window Message. If you need to understand more about how to form these messages, read my essay on Message Management.
The message handler is a function whose signature is as shown. The WPARAM and LPARAM of the PostThreadMessage are passed to the handler method. The return value is always ignored, and is traditionally 0.
You can queue messages up for processing simply by using
PostThreadMessage. You don't need a queue, interlocks, or any other mechanism to handle queuing. However, because you are doing
PostThreadMessage, you must make sure that the object you are passing is still in existence at the time you process the message. This means that if you are not passing simple integer values as
LPARAM, for example, if you want to pass a
CString, you must pass a pointer to an object on the heap and delete it when you've processed the message. You cannot pass a pointer to anything on the stack.
However, this also suggests that a "user-interface" thread can also be thought of as a "queued event thread", where you use PostThreadMessage to add entries to the queue, and dispatch the dequeuing via the
Terminating the loop
So we know how to start the thread, and initialize the thread, but how do we stop the thread? Just like any other message pump, as it turns out. Just post a
WM_QUIT message. It is unfortunate that Microsoft has not chosen to be consistent, but they actually require in the PostThreadMessage that you explicitly provide the
LPARAM values, which are, in this case, 0. So when you get to the point where you are read to shut down the thread, just do
thread->PostThreadMessage(WM_QUIT, 0, 0);
and the thread shuts down.
It is a bad idea to mix thread control using
PostThreadMessage and UI objects such as message boxes and dialog boxes. When a thread message is received by
GetMessage, it has a
NULL window handle. This means that it cannot be dispatched via
DispatchMessage. Special handling is required for thread messages, which is built into the message pump of
CWinThread::Run. What this means is that if you call
MessageBox, or any similar function that spawns a new message loop any posted thread messages will be lost. Microsoft suggests using a message hook function to intercept messages under these conditions. Consequently, if you are going to use
PostThreadMessage to a UI thread, you should follow the advice given in my essay about worker threads and treat the UI thread as if it were a worker thread insofar as spawning child dialogs.
The views expressed in these essays are those of the author, and in no way represent, nor are they endorsed by, Microsoft.
Send mail to firstname.lastname@example.org with questions or comments about this article.
Copyright © 1999 The Joseph M. Newcomer Co. All Rights Reserved.