The need to keep a user interface responsive at all times while simultaneously allowing for long-running tasks is something that comes up again and again in Windows development. Even with all of the improvements since Windows 16 in this area (particularly the availability of separate threads of execution), you may often find yourself needing to perform the tasks in the primary application thread. This may be due to legacy or third-party code that is not threadsafe (i.e., doesn't guard access to shared state). But your users demand that your application continue to be responsive even while these tasks execute. The question is then how to satisfy both needs without putting the tasks into background threads.
Fortunately, MFC comes to the rescue with user interface threads.
These are managed in MFC with the CWinThread
class. The CWinApp
class that every MFC application has an instance of also derives from CwinThread
,
which provides for a message pump and handling message in the queue. In order
to keep a user interface responsive, its necessary to periodically check
the queue for available messages and dispatch them to the appropriate windows.
If a task in the primary (CWinApp
) CWinThread
instance takes too
much time, messages can back up in the queue; this is what causes an unresponsive
application.
Creating additional CWinThread
s is straightforward as long as you think
through how to initialize and later terminate any of these objects. Usually
this requires use of synchronization objects such as Win32 events (we covered
these in previous newsletters). Lets take a look at some code that demonstrates
one possible approach for managing CWinThread
s to keep the UI responsive.
First, we need to derive a class from CWinThread
that will contain some
value-added extensions well need:
class CUIThread : public CWinThread { public: DECLARE_DYNCREATE(CUIThread) CUIThread(); // Attributes public: HANDLE m_hEventKill; HANDLE m_hEventDead; // Operations public: void KillThread(); // Overrides // ClassWizard generated virtual function overrides //{{AFX_VIRTUAL(CGDIThread) //}}AFX_VIRTUAL // Implementation public: virtual ~CUIThread(); virtual void Delete(); protected: virtual BOOL InitInstance(); // Generated message map functions //{{AFX_MSG(CUIThread) // NOTE - the ClassWizard will add and remove member functions here. //}}AFX_MSG DECLARE_MESSAGE_MAP() };
Looking through this class, we see that there are two new member variables
named m_hEventKill
and m_hEventDead
. The
first variable is used to signal that the thread should kill itself (more on
this in a bit), and the second variable is used to signal that the thread is
really dead.
In addition, the virtual methods InitInstance
and Delete
are
overridden from the base CWinThread
class. The former is used to spin
up the thread and the latter is used as a hook to know when the thread really
dies. You wont find Delete
documented in the MSDN help documentation,
but it does existjust take a peek at the afxwin.h
MFC file for
more details on the layout of CWinThread
.
Next, lets take a look at some code for the new CUIThread
class:
#include "stdafx.h" #include "UIThread.h" #ifdef _DEBUG #undef THIS_FILE static char BASED_CODE THIS_FILE[] = __FILE__; #endif ///////////////////////////////////////////////////////////////////////////// // CUIThread IMPLEMENT_DYNCREATE(CUIThread, CWinThread) BEGIN_MESSAGE_MAP(CUIThread, CWinThread) //{{AFX_MSG_MAP(CUIThread) // NOTE - the ClassWizard will add and remove mapping macros here. //}}AFX_MSG_MAP END_MESSAGE_MAP() CUIThread::CUIThread() { m_bAutoDelete = FALSE; // kill event starts out in the signaled state m_hEventKill = CreateEvent(NULL, TRUE, FALSE, NULL); m_hEventDead = CreateEvent(NULL, TRUE, FALSE, NULL); } BOOL CUIThread::InitInstance() { // wait for the kill notification. service the // message queue. while (WaitForSingleObject(m_hEventKill, 0) == WAIT_TIMEOUT) ; // avoid entering standard message loop by returning FALSE return FALSE; } void CUIThread::Delete() { // calling the base here won't do anything but it is a // good habit CWinThread::Delete(); // acknowledge receipt of kill notification VERIFY(SetEvent(m_hEventDead)); } CUIThread::~CUIThread() { CloseHandle(m_hEventKill); CloseHandle(m_hEventDead); } void CUIThread::KillThread() { // Note: this function is called in the context of // other threads, not the thread itself. // reset the m_hEventKill which signals the thread // to shutdown VERIFY(SetEvent(m_hEventKill)); // allow thread to run at higher priority during // kill process SetThreadPriority(THREAD_PRIORITY_ABOVE_NORMAL); WaitForSingleObject(m_hEventDead, INFINITE); WaitForSingleObject(m_hThread, INFINITE); // now delete CWinThread object since no longer necessary delete this; }
Lets walk through the code a bit at this point. In the constructor, we mark the object so that it doesnt auto-delete (this normally occurs when the thread goes away). Were not allowing an auto-delete so that we can handle the destruction sequence a bit more manually. This isnt required, but should give you an idea of what is possible. We also create two events that will be used to know when to kill the thread and when its actually dead.
Next, the InitInstance
method goes into an infinite wait state on the
kill event. This allows messages to be processed since the thread is not doing
anything while waiting for the event to be signaled. When the kill event is
finally signaled, the while loop is exited and the method returns FALSE to indicate
that the thread is exiting. Kind of strange, but thats how Microsoft designed
this class to work.
Next, the Delete
method calls the base method (which does nothing, but
is good practice) and then signals the dead event. This tells other
code in this class that the thread is really dead and not just exiting.
Finally, well look at the KillThread
method. This is really the
main external method of the class and is what callers interact with. Once the
thread is up and running, a user of this class would call the KillThread
method to force the thread to exit gracefully. The first step is to signal the
InitInstance
method to stop waiting. It then boosts the thread priority
slightly before going into a wait state on the dead event. Why boost
the thread priority? Well, this is one of the few cases where Ive had
to consider changing thread priorities, but Ive found that doing so allows
the thread to exit more quickly than if the thread continues to run at its original
priority. Usually I want the thread to terminate immediately, but given that
its a thread, we might have to wait a bit for it to get a time slice from
the OS. Next, we wait on the dead event to get signaled from the
Delete
method. Finally, we wait on the m_hThread
handle to be
signaled which means the thread has really gone away. Before we leave the method,
we cleanup by deleting ourself since we had turned off auto-delete earlier.
To use this class we might do the following:
// create UI thread CUIThread* pUIThread = static_cast< CUIThread*>( AfxBeginThread(RUNTIME_CLASS(CUIThread)) ); // do something that takes a long time. . . // terminate the UI thread. pUIThread->KillThread();
Pretty simple. As you can see, its possible to integrate a UI thread into procedural code to allow your UI to continue to be responsive without resorting to more complex threading. I wouldnt recommend you do this as a way around threading in general, but for special cases where its unavoidable, it may be your only option.
Mark M. Baker is the Chief of Research & Development at BNA Software located in Washington, D.C. [email protected].