Estragon: "Let's go."
Vladimir: "We can't; we're waiting for Godot."
It's inevitable I guess, but when developing GUIs we seem to encounter many of the same problems over and over. How many times have you had to write GUI code that makes an ordered sequence of synchronous calls to some external service or other (server, OS, mid-tier or hardware) and had to contrive a way make the GUI responsive while waiting for the call to complete? The GUI is at the mercy of how long the call takes and, if lengthy, the end-user can't distinguish from a hung application and one that's just taking a long time. In many cases, the call time may be non-deterministic by its very nature. Regardless, the onus is on the GUI developer to insulate the user from this and not have the GUI partly repainting, whiting out and generally behaving poorly.
I'm sure you've tried all the techniques, from making the calls in separate threads to wrapping them in classes that fire events when the call is complete. All the techniques I've seen use some combination of events, threads and state machine approaches, but all this leads to code that is much more complicated that it needs to be. Fundamentally, you are just making a sequential series of synchronous calls.
In this article, I am going to look at how to address this problem in a straightforward way, making your code readable and very maintainable.
Pump Me Baby One More Time
All windows and controls use an event-driven messaging infrastructure to handle the things that happen: resizing, mouse clicks, paint requests, etc. These events are represented and implemented using window messages. Each window has a Windows procedure (the infamous Wndproc) that is responsible for processing the messages.
Part of the application's responsibility is to actively get messages from the Windows system and dispatch them to their destination. This is commonly referred to as a "message pump" or "message loop." The main GUI thread that the application runs in owns all of the windows it pumps messages for. The methods called by WndProc will be in the same thread. A simple message pump looks something like this.
while(GetMessage(&Msg, NULL, 0, 0) > 0)
{
TranslateMessage(&Msg);
DispatchMessage(&Msg);
}
So if a synchronous method called in the main GUI thread blocks for long periods, the message pump won't be called. The paint messages won't be processed and you'll end up with screen whiteout, an unresponsive GUI and a grumpy user.
Here endeth the message pump lesson!
A Typical Problem
Let's assume you have a GUI that needs to make the following calls in sequence.
void CPricingServerDlg::OnStartBadSimulation()
{
LPVOID params = 0;
NewPricingJob(params);
ReadSimulationParameters(params);
RunLocally(params);
WriteExposureData(params);
WriteResults(params);
}
We don't know how long each of these calls will take. They may range from sub-second to many seconds, but we don't want the message pump to be blocked by them. Also, we don't want the application code to end up being overly complicated and difficult to read or follow. My solution is a simple-to-use template class wrapped up in a macro called SyncInvoker
.
#ifndef __SYNCINVOKE_H__
#define __SYNCINVOKE_H__
#include "atlbase.h"
namespace SyncInvoker
{
class Thread
{
public :
Thread(UINT uThrdDeadMsg = 0) : m_uThreadDeadMsg(uThrdDeadMsg) {}
virtual ~Thread() {CloseHandle(m_hThread);}
virtual Thread &Create(LPSECURITY_ATTRIBUTES lpThreadAttributes =
0, DWORD dwStackSize = 0, DWORD dwCreationFlags = 0, UINT
uThrdDeadMsg = 0)
{
if (uThrdDeadMsg) m_uThreadDeadMsg = uThrdDeadMsg;
m_dwCreatingThreadID = GetCurrentThreadId();
m_hThread = CreateThread(lpThreadAttributes, dwStackSize,
ThreadProc, reinterpret_cast(this), dwCreationFlags,
&m_dwThreadId);
return *this;
}
bool Valid() const {return m_hThread != NULL;}
DWORD ThreadId() const {return m_dwThreadId;}
HANDLE ThreadHandle() const {return m_hThread;}
protected :
virtual DWORD ThreadProc() {return 0;}
DWORD m_dwThreadId;
HANDLE m_hThread;
DWORD m_dwCreatingThreadID;
UINT m_uThreadDeadMsg;
static DWORD WINAPI ThreadProc(LPVOID pv)
{
if (!pv) return 0;
DWORD dwRet = reinterpret_cast(pv)->ThreadProc();
if (reinterpret_cast(pv)->m_uThreadDeadMsg)
PostThreadMessage(reinterpret_cast(pv)->m_dwCreatingThreadID,
reinterpret_cast(pv)->m_uThreadDeadMsg, 0, dwRet);
return dwRet;
}
};
template <class T_OWNER>
class CSyncCall : public Thread
{
public:
CSyncCall():m_owner(NULL), m_method(NULL),m_param(0){}
typedef void (T_OWNER::*METHOD_PTR)(LPVOID);
bool Call(T_OWNER *owner,METHOD_PTR method,LPVOID param)
{
m_owner = owner;
m_method = method;
m_param = param;
return Create().Valid();
}
virtual DWORD ThreadProc()
{
if(m_owner)
(m_owner->*m_method)(m_param);
return 0;
}
private:
T_OWNER *m_owner;
METHOD_PTR m_method;
LPVOID m_param;
};
}
#define SyncInvoke(cls, method, param)
{
SyncInvoker::CSyncCall syncCall;
syncCall.Call(this, method, param);
AtlWaitWithMessageLoop(syncCall.ThreadHandle());
}
#endif
SyncInvoker
uses a class called CSyncCall
and Thread
to invoke the required call in a separate thread; then it waits until the call is complete. While waiting, it keeps the messages pumping using AtlWaitWithMessageLoop
. The macro allows for parameters to be passed to the threaded call and could be used for returning success code. Although this may look complicated, it's pretty straightforward and is all hidden away in the macro.
Despite this template and macro looking complicated (it's not really), the series of synchronous calls that were shown above are simply wrapped in the macro to make them well-behaved in the GUI.
void CPricingServerDlg::OnStartGoodSimulation()
{
LPVOID params = 0;
SyncInvoke(CPricingServerDlg,
&CPricingServerDlg::NewPricingJob, ¶ms);
SyncInvoke(CPricingServerDlg,
&CPricingServerDlg::ReadSimulationParameters, ¶ms);
SyncInvoke(CPricingServerDlg, &CPricingServerDlg::RunLocally, ¶ms);
SyncInvoke(CPricingServerDlg,
&CPricingServerDlg::WriteExposureData, ¶ms);
SyncInvoke(CPricingServerDlg, &CPricingServerDlg::WriteResults, ¶ms);
}
The Sample Application
I've provided a sample application called PricingServer
to demonstrate how to use SyncInvoker
. Once you've built the sample, run it and hit the "Badly behaved GUI" button. It launches a window for each of the lengthy calls. Because it's not using SyncInvoker
, you'll find that you can't move the window around, that the progress bar doesn't move and if you drag another window over it, it doesn't get repainted until the call has finished: a nasty GUI. Try the same with the "Well behaved GUI" button and everything behaves as it should. The window paints and can be moved while the synchronous calls are being made.
Some Points to Note
- Since the message pump is no longer blocked when using
SyncInvoker
, your GUI will be active during the calls. Consequently, all the buttons can be pressed and their handlers will be called. You need to make sure you take account of this and lock out controls appropriately. - Similarly, a user can close the application mid-call.
- The
Start
and StopProgress
methods are there to show a progress bar doing something based on a WM_TIMER
. Again, this is blocked by synchronous calls. - You may feel the need to adapt
AtlWaitWithMessageLoop
(in atlbase.inl) so that it doesn't use INFINITE, but some timeout value in milliseconds so a timeout can be imposed on the call. If you do, remember you'll need to take account of the failed call and all that's associated with it.