Click here to Skip to main content
15,867,308 members
Articles / Desktop Programming / MFC
Article

SyncInvoker: Keep Your GUI Responsive During Lengthy Synchronous Calls

Rate me:
Please Sign up or sign in to vote.
4.93/5 (20 votes)
16 Apr 2007CPOL4 min read 68.2K   461   54   19
Keep your GUI responsive when making blocking synchronous calls. Dave offers a technique using SyncInvoker,
Screenshot - SyncInvoker.jpg

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.

C++
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.

C++
#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.

C++
void CPricingServerDlg::OnStartGoodSimulation()
{
    LPVOID params = 0;

    SyncInvoke(CPricingServerDlg, 
        &CPricingServerDlg::NewPricingJob, &params);
    SyncInvoke(CPricingServerDlg, 
        &CPricingServerDlg::ReadSimulationParameters, &params);
    SyncInvoke(CPricingServerDlg, &CPricingServerDlg::RunLocally, &params);
    SyncInvoke(CPricingServerDlg, 
        &CPricingServerDlg::WriteExposureData, &params);
    SyncInvoke(CPricingServerDlg, &CPricingServerDlg::WriteResults, &params);
}

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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Technical Lead
United Kingdom United Kingdom
When Dave isn't trying to play drums, fly helicopters or race cars, he can be found coding hard and herding cats at JoinIn Networks He must try harder!

You can read Dave's ramblings in his blog Aliens Ate My GUI

Or, if you can be bothered, he twitters on BoomerDave

Comments and Discussions

 
General2 SyncInvoke calls from one method are serial. Pin
alex.buzunov30-Jan-10 7:18
alex.buzunov30-Jan-10 7:18 
QuestionHow do you call method from another class. Pin
BhushanKalse4-Jul-07 22:02
BhushanKalse4-Jul-07 22:02 
AnswerRe: How do you call method from another class. Pin
eFotografo27-Mar-12 22:58
professionaleFotografo27-Mar-12 22:58 
GeneralAtlWaitWithMessageLoop replacement Pin
Shao Voon Wong24-Apr-07 16:15
mvaShao Voon Wong24-Apr-07 16:15 
GeneralSuggestion Pin
c2j223-Apr-07 20:34
c2j223-Apr-07 20:34 
GeneralVery good Pin
NigelVer18-Apr-07 22:16
NigelVer18-Apr-07 22:16 
GeneralSome comment. [modified] Pin
JaeWook Choi18-Apr-07 5:13
JaeWook Choi18-Apr-07 5:13 
GeneralRe: Some comment. Pin
David M Brooks18-Apr-07 6:04
David M Brooks18-Apr-07 6:04 
GeneralRe: Some comment. [modified] Pin
JaeWook Choi18-Apr-07 9:43
JaeWook Choi18-Apr-07 9:43 
GeneralThe devil is in the details... Pin
charlieg18-Apr-07 4:19
charlieg18-Apr-07 4:19 
GeneralRe: The devil is in the details... Pin
David M Brooks18-Apr-07 4:46
David M Brooks18-Apr-07 4:46 
GeneralRe: The devil is in the details... Pin
charlieg18-Apr-07 5:13
charlieg18-Apr-07 5:13 
GeneralExcellent Post Pin
stuartmurray18-Apr-07 3:36
stuartmurray18-Apr-07 3:36 
GeneralMisnomer Pin
sadavoya17-Apr-07 10:44
sadavoya17-Apr-07 10:44 
GeneralRe: Misnomer Pin
David M Brooks17-Apr-07 22:35
David M Brooks17-Apr-07 22:35 
GeneralWOW! I have been looking for this... Pin
hannahb17-Apr-07 4:59
hannahb17-Apr-07 4:59 
GeneralRe: WOW! Small Rewrite Pin
hannahb17-Apr-07 5:45
hannahb17-Apr-07 5:45 
QuestionSyntax error? Pin
Jerry Evans17-Apr-07 3:20
Jerry Evans17-Apr-07 3:20 
AnswerRe: Syntax error? Pin
David M Brooks17-Apr-07 4:51
David M Brooks17-Apr-07 4:51 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.