4 Asynchronous UI

A modern UI should avoid showing wait cursors to the user, and where necessary perform time consuming activities such as computation in a worker thread. The main thread should also tend to avoid performing I/O - particularly if it involves communication over a network. Otherwise the UI can feel unresponsive or sluggish to the user. Even blocking the main thread for just 50 milliseconds can impact the rate at which data entry can be performed by users.

Posting tasks to the main UI thread

It is useful for application programs to have a means to allow any thread to post a lambda function to a queue which is executed some time in the future by the main UI thread. This might have the following signature:


void PostTaskToMainUI( std::function<void()> task );

For example, a Windows application could implement this function in terms of the Win32 function PostMessageW, perhaps using WM_USER for the Msg parameter:


BOOL PostMessageW(
  [in, optional] HWND   hWnd,
  [in]           UINT   Msg,
  [in]           WPARAM wParam,
  [in]           LPARAM lParam
);

Posting tasks to a worker thread

Consider that there is a function available to any thread to post tasks to a worker thread (or a thread pool):


void PostTaskToWorkerThread( std::function<void()> task );

This may cause the posted tasks to be queued.

Synchronous version

Let there be a pure function which calculates an output for a given input:


Output CalculateOutput(Input input);

Consider the following event handler which is called by the main UI thread and which retrieves the input (perhaps from a model in an MVC design), directly calls CalculateOutput then updates the UI (perhaps by updating derived variables in a model):


// Synchronous version which might make the UI feel unresponsive
void MyUI::OnMouseButtonUp()
{
    Input input = CalculateInput();
    Output output = CalculateOutput(input);
    UpdateUI(output);
}

Asynchronous version

Let's assume CalculateOutput takes a long time, so that we cannot afford to call it from the main UI thread. Fortunately, since this is a pure function which doesn't access any global state, it can safely be called by a worker thread. Furthermore it is assumed the types Input and Output are value types supporting default construction, copy and move construction and copy and move assignment.


// Asynchronous version which makes the UI very responsive
void MyUI::OnMouseButtonUp()
{
    Input input = CalculateInput();
    PostTaskToWorkerThread(
        [input]()
        {
            Output output = CalculateOutput(input);
            PostTaskToMainUI(
                [output]()
                {
                    UpdateUI(output);
                });
        });
}

Note the following:

  • Typically OnMouseButtonUp executes extremely quickly (perhaps in a few microseconds)
  • The input is only retrieved by the main UI thread and the UI is only updated by the main UI thread. This is important because only the main UI thread is allowed to access the model, view and controller objects of the UI.
  • The worker thread is provided its own private copy of both the input and the output, there is no shared state involved with running the CalculateOutput function.
  • For performance, it is straightforward to use move semantics for passing the input from the main UI thread to the worker thread, and for passing the output from the worker thread to the main UI thread. Indeed for this reason it is useful to use data types which have efficient move operations.