GUI Thread

GUI Thread#

Problem

A user interface thread must remain responsive to user requests, and must not get bogged down in long computations.

Context

Graphical user interfaces often have a dedicated thread (“GUI thread”) for servicing user interactions. The thread must remain responsive to user requests even while the application has long computations running. For example, the user might want to press a “cancel” button to stop the long running computation. If the GUI thread takes part in the long running computation, it will not be able to respond to user requests.

Forces

  • The GUI thread services an event loop.

  • The GUI thread needs to offload work onto other threads without waiting for the work to complete.

  • The GUI thread must be responsive to the event loop and not become dedicated to doing the offloaded work.

Related

  • Non-Preemptive Priorities

  • Local Serializer

Solution

The GUI thread offloads the work by firing off a task to do it using method task_arena::enqueue of a task_arena instance. When finished, the task posts an event to the GUI thread to indicate that the work is done. The semantics of enqueue cause the task to eventually run on a worker thread distinct from the calling thread.

The following figure sketches the communication paths. Items in black are executed by the GUI thread; items in blue are executed by another thread.

image0

Example

The example is for the Microsoft Windows* operating systems, though similar principles apply to any GUI using an event loop idiom. For each event, the GUI thread calls a user-defined function WndProc to process an event.

// Event posted from enqueued task when it finishes its work.
const UINT WM_POP_FOO = WM_USER+0;


// Queue for transmitting results from enqueued task to GUI thread.
oneapi::tbb::concurrent_queue<Foo>ResultQueue;


// GUI thread's private copy of most recently computed result.
Foo CurrentResult;


LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) {
   switch(msg) {
       case WM_COMMAND:
           switch (LOWORD(wParam)) {
               case IDM_LONGRUNNINGWORK:
                   // User requested a long computation. Delegate it to another thread.
                   LaunchLongRunningWork(hWnd);
                   break;
               case IDM_EXIT:
                   DestroyWindow(hWnd);
                   break;
               default:
                   return DefWindowProc(hWnd, msg, wParam, lParam);
           }
           break;
       case WM_POP_FOO:
           // There is another result in ResultQueue for me to grab.
           ResultQueue.try_pop(CurrentResult);
           // Update the window with the latest result.
           RedrawWindow( hWnd, NULL, NULL, RDW_ERASE|RDW_INVALIDATE );
           break;
       case WM_PAINT:
           Repaint the window using CurrentResult
           break;
       case WM_DESTROY:
           PostQuitMessage(0);
           break;
       default:
           return DefWindowProc( hWnd, msg, wParam, lParam );
   }
   return 0;
}

The GUI thread processes long computations as follows:

  1. The GUI thread calls LongRunningWork, which hands off the work to a worker thread and immediately returns.

  2. The GUI thread continues servicing the event loop. If it has to repaint the window, it uses the value ofCurrentResult, which is the most recent Foo that it has seen.

When a worker finishes the long computation, it pushes the result into ResultQueue, and sends a message WM_POP_FOO to the GUI thread.

  1. The GUI thread services a WM_POP_FOO message by popping an item from ResultQueue into CurrentResult. The try_pop always succeeds because there is exactly one WM_POP_FOO message for each item in ResultQueue.

Routine LaunchLongRunningWork creates a function task and launches it using method task_arena::enqueue.

class LongTask {
   HWND hWnd;
   void operator()() {
       Do long computation
       Foo x = result of long computation
       ResultQueue.push( x );
       // Notify GUI thread that result is available.
       PostMessage(hWnd,WM_POP_FOO,0,0);
   }
public:
   LongTask( HWND hWnd_ ) : hWnd(hWnd_) {}
};

void LaunchLongRunningWork( HWND hWnd ) {
   oneapi::tbb::task_arena a;
   a.enqueue(LongTask(hWnd));
}

It is essential to use method task_arena::enqueue here. Even though, an explicit task_arena instance is created, the method enqueue ensures that the function task eventually executes when resources permit, even if no thread explicitly waits on the task. In contrast, oneapi::tbb::task_group::run may postpone execution of the function task until it is explicitly waited upon with the oneapi::tbb::task_group::wait.

The example uses a concurrent_queue for workers to communicate results back to the GUI thread. Since only the most recent result matters in the example, and alternative would be to use a shared variable protected by a mutex. However, doing so would block the worker while the GUI thread was holding a lock on the mutex, and vice versa. Using concurrent_queue provides a simple robust solution.

If two long computations are in flight, there is a chance that the first computation completes after the second one. If displaying the result of the most recently requested computation is important, then associate a request serial number with the computation. The GUI thread can pop from ResultQueue into a temporary variable, check the serial number, and update CurrentResult only if doing so advances the serial number.