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
OnMouseButtonUpexecutes 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
CalculateOutputfunction. - 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.