Using User-Interface Threads |
|
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 a 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.
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.
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 CMyThread.
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),
THREAD_PRIORITY_NORMAL,
0, // stack size
CREATE_SUSPENDED);
And upon completion, you can then set any member variables in the class, and explicitly resume the thread:
thread->m_Whatever = ...;
thread->m_OtherThing = ...;
thread->ResumeThread();
I chose to do the two-step method suggested in the AfxBeginThread documentation:
CMyThread * thread = new CMyThread();
thread->m_Whatever = ...;
thread->m_OtherThing = ...;
thread->CreateThread();
I did this just because I did not like to explicitly specify the defaults.
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.
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 FALSE.
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.
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))
The 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 WPARAM or 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 MESSAGE_MAP.
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 WPARAM and 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 DoModal or AfxMessageBox, 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.
If you want to run Speech API (SAPI) in a separate thread, that thread must be a UI thread. If you want to run CAsyncSocket in a thread, that thread must be a UI thread. In the case of CAsyncSocket, there are additional considerations about passing the socket from the creating thread to the UI thread, which I discuss in a separate essay.
10-Jan-03 Added discussion about when UI threads are mandatory
The views expressed in these essays are those of the author, and in no way represent, nor are they endorsed by, Microsoft.