Building Responsive GUI Applications with PPL Tasks

In my last post I introduced the concepts of PPL tasks and continuations, and showed how to use them to orchestrate the work between the dependent tasks. I also alluded to the usefulness of the model in wait-free programming, and in particular, building responsive GUI applications.

Don’t get me wrong – PPL does not purport to be a GUI programming library. If you want to build a rich client app in C++, you have a vast array of frameworks at your disposal – from MFC to WTL to QT etc. But, PPL can be used in conjunction with other libraries, or with plain old Win32.

The key to making the UI responsive is returning control to the message loop as soon as possible, offloading blocking or CPU-intensive operations to other threads.

However, updating the UI objects with the data produced by a background thread can only be done on the main UI thread – the thread on which you created these UI objects. This means that you need to join the background thread to the main UI thread, or somehow marshal the data from one thread to another.

I’ll start with how not to do it (you will see the full sample, called GUI_Tasks, in the Sample Pack):

    1: task<int> fibTask([=]()
    2: {
    3:     return fib(40);
    4: });
    5:  
    6: fibTask.wait(); // bad! blocks the main thread
    7:
    8: n = fibTask.get();

Because no useful work is being done between lines 5 and 6, creating a task here doesn’t help at all.

Fortunately, there is a couple of solutions.

Pumping Wait

The most obvious solution is to modify the wait call above to allow the thread to pump messages while it is waiting for the task to complete. We call such a wait a “pumping wait” or a “cowait”. Here is how I implemented it:

    1: template<typename T>
    2: void cowait( task<T> task )
    3: {
    4:     HANDLE handle = CreateEvent(NULL, FALSE, FALSE, L"task_event");
    5:  
    6:     task.continue_with( [=](T t) 
    7:     {
    8:         SetEvent(handle);
    9:     });
   10:  
   11:     MSG msg;
   12:     while( true )
   13:     {
   14:         DWORD dwResult = MsgWaitForMultipleObjects(1, &handle, FALSE, INFINITE, QS_ALLEVENTS );
   15:  
   16:         if( dwResult == WAIT_OBJECT_0+1 )
   17:         {
   18:             // Check if message arrived or not
   19:             if(PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
   20:             {
   21:                 // Translate and dispatch message
   22:                 TranslateMessage(&msg);
   23:                 DispatchMessage(&msg);
   24:             }
   25:         }
   26:         else
   27:         {
   28:             CloseHandle(handle);
   29:             break;
   30:         }
   31:     }
   32: }

The implementation creates an event, and attaches a continuation to the given task, which sets the event when the task completes. It then calls the MsgWaitForMultipleObjects function, which returns when either a message appears in the thread’s input queue, or the event is set. If a message comes in, the implementation dispatches it to the window procedure, and repeats the loop. If the event is set, it closes the handle and gets out of the cowait – calling the caller’s code immediately following the cowait call.

The call site now looks like this:

    1: clock_t ticks0 = clock();
    2: task<int> fibTask([=]()
    3: {
    4:     return fib(40);
    5: });
    6:  
    7: cowait(fibTask); // good, a non-blocking call
    8:  
    9: n = fibTask.get();

 

Continue On UI Thread

If you have fully embraced the mental model of continuations, an alternative solution to the above is to retrieve the results of the fibTask in a continuation, but have that continuation run on the UI thread.

The call site looks like this:

    1: task<int> fibTask([=]()
    2: {
    3:     return fib(40);
    4: });
    5:  
    6: continue_with_on_ui_thread(fibTask, [=](int n)
    7: {
    8:     // use n here
    9: });

Unlike cowait, continue_on_ui_thread can be made to return a task, which can be composed with other tasks.

In the interest of brevity, I will omit the implementation here and encourage you to look it up in the sample pack. Suffices to say that the approach is very similar to that of the cowait above.

Reentrancy

Finally, a word of caution.

Both cowait and continue_with_on_ui_thread start a new message loop and dispatch messages back into the window procedure that called them, causing a synchronous reentry into your window procedure.

Thus, your window procedure must be designed to be reentrant. Avoiding the most common reentrancy pitfalls is not hard. For example, if your code relies on some mutable global state, a call to cowait can overwrite that global state, potentially leading to incorrect behavior.

Artur Laksberg