Preliminary. ThinkMeta ConcurrentTasks and this documentation are preliminary; everything is subject to change.

Waiting on a UI thread

On this page

A click starts work that takes a while. The handler should hand the work to worker threads, wait for it and then show the result, and the window should stay responsive meanwhile. With a dispatcher scheduler the handler is written as one ordinary function. The examples come from guide-ui-thread.cpp, a complete Win32 program that clicks its own button once and checks the result.

#include <ThinkMeta.ConcurrentTasks/Core.h>
import ThinkMeta.ConcurrentTasks.Schedulers.DispatcherScheduler;
import ThinkMeta.ConcurrentTasks.Synchronization.TaskGroup;
import ThinkMeta.ConcurrentTasks.Tasks.Task;

namespace TC = ThinkMeta::ConcurrentTasks;

Two schedulers

The work runs on a Default scheduler, with one thread less than the logical processors so that the UI thread keeps one. The handlers run on a dispatcher scheduler bound to the UI thread. Schedulers::DispatcherScheduler owns it and posts every task that can go on to a window:

int main()
{
    // The workers: one thread less than the logical processors, so that the UI keeps one.
    const DWORD       processors = GetActiveProcessorCount(ALL_PROCESSOR_GROUPS);
    TC::SchedulerInfo info{.numberOfThreads = processors > 1 ? processors - 1 : 1};
    ui.workers = TC::CreateTaskScheduler(TC::SchedulerKind::Default, &info);
    if (ui.workers == nullptr)
        return 1;
    ui.workers->Start();

    WNDCLASSEXW wc{sizeof(wc)};
    wc.lpfnWndProc   = WindowProc;
    wc.hInstance     = GetModuleHandleW(nullptr);
    wc.hbrBackground = reinterpret_cast<HBRUSH>(COLOR_BTNFACE + 1);
    wc.lpszClassName = L"GuideUiThread";
    RegisterClassExW(&wc);
    ui.window = CreateWindowExW(0, wc.lpszClassName, L"Waiting on a UI thread", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT,
                                CW_USEDEFAULT, 400, 160, nullptr, nullptr, wc.hInstance, nullptr);
    ui.button = CreateWindowExW(0, L"BUTTON", L"Sum", WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, 20, 20, 100, 30, ui.window,
                                reinterpret_cast<HMENU>(static_cast<INT_PTR>(IdSum)), wc.hInstance, nullptr);
    ShowWindow(ui.window, SW_SHOWNOACTIVATE);

    {
        // Bound to this thread. A task that can go on is posted to the window as WM_DISPATCH.
        TC::Schedulers::DispatcherScheduler dispatcher(ui.window, WM_DISPATCH);
        ui.dispatcher = &dispatcher;

        PostMessageW(ui.window, WM_COMMAND, IdSum, 0); // click once
        MSG msg;
        while (GetMessageW(&msg, nullptr, 0, 0) > 0) {
            TranslateMessage(&msg);
            DispatchMessageW(&msg);
        }
    } // the dispatcher goes before the workers; no handler waits any more

    ui.workers->Stop();
    ui.workers->Destroy();

    constexpr long long Count    = 400'000'000;
    const bool          sumOk    = ui.total == Count * (Count - 1) / 2;
    const bool          threadOk = ui.startedOn == ui.endedOn && ui.startedOn == GetCurrentThreadId();
    std::printf("sum %lld (%s), handler continued on the UI thread: %s\n", ui.total, sumOk ? "ok" : "WRONG", threadOk ? "yes" : "NO");
    return sumOk && threadOk ? 0 : 1;
}

Create the dispatcher on the UI thread and destroy it there, after the message loop has ended and no handler waits any more. Destroy it before the workers.

The window procedure

Two messages connect the window to the dispatcher. A click starts the handler with RunTask, and WM_DISPATCH continues a handler with ContinueTask:

LRESULT CALLBACK WindowProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) noexcept
{
    switch (message) {
    case WM_COMMAND:
        if (LOWORD(wParam) == IdSum) {
            ui.dispatcher->RunTask(&sumHandler); // returns at the handler's first wait
            return 0;
        }
        break;

    case WM_DISPATCH:
        ui.dispatcher->ContinueTask(lParam); // a waiting handler goes on, on this thread
        return 0;

    case WM_CLOSE:
        if (ui.busy)
            return 0; // keep the window while the handler waits
        break;

    case WM_DESTROY:
        PostQuitMessage(0);
        return 0;
    }
    return DefWindowProcW(hwnd, message, wParam, lParam);
}

RunTask returns when the handler ends or at its first wait. Either way the window procedure returns at once, and the message loop goes on.

Decide per message whether its handler may wait:

Messages Handler Why
Clicks, menu commands, accelerators asynchronous: RunTask user actions whose result appears "later"
WM_SIZE, WM_PAINT, WM_NCHITTEST, WM_SETCURSOR, WM_GETMINMAXINFO synchronous: an ordinary call Windows needs the answer at once
Keys, mouse buttons, mouse moves synchronous their order matters; let them raise a command that may wait

The handler

The handler is the Run of a task. It splits the work into parts, adds them to the workers, and waits for the group:

// One part of the work: sums a range of numbers on a worker thread.
class PartTask : public TC::Tasks::Task
{
public:
    void Run() noexcept override
    {
        sum = 0;
        for (long long n = first; n < end; ++n)
            sum += n;
    }

    long long first = 0;
    long long end   = 0;
    long long sum   = 0;
};
// The click handler. It runs as a task of the dispatcher scheduler on the UI thread, so it
// may wait: at the first wait the window procedure returns, and the UI keeps running.
void OnSum() noexcept
{
    ui.busy = true;
    EnableWindow(ui.button, FALSE); // a click during the wait would start the handler again
    ui.startedOn = GetCurrentThreadId();

    constexpr int       Parts = 8;
    constexpr long long Count = 400'000'000;
    PartTask            parts[Parts];
    TC::Synchronization::TaskGroup_s group;
    for (int i = 0; i < Parts; ++i) {
        parts[i].first = Count / Parts * i;
        parts[i].end   = Count / Parts * (i + 1);
        group.AddTask(&parts[i]);
        parts[i].Schedule(*ui.workers); // the workers - not Schedule(), that is the UI thread
    }
    group.Wait(); // suspends the handler, not the thread

    // back on the UI thread
    ui.endedOn = GetCurrentThreadId();
    ui.total   = 0;
    for (const PartTask& part : parts)
        ui.total += part.sum;
    SetWindowTextW(ui.window, L"Done");
    EnableWindow(ui.button, TRUE);
    ui.busy = false;

    DestroyWindow(ui.window); // the sample ends after one click
}

class HandlerTask : public TC::Tasks::Task
{
public:
    void Run() noexcept override { OnSum(); }
} sumHandler; // reused for every click; the disabled button keeps it from running twice

Warning

Add the work with Task::Schedule(scheduler) or AddTask on the workers. Schedule() without a scheduler adds to the calling task's scheduler, the dispatcher, and the work would run on the UI thread.

For a single piece of work, Tasks::RunAndWait does the same in one call:

TC::Tasks::RunAndWait(*ui.workers, [&] { result = Compute(input); });

The parts may live on the handler's stack: it waits until all of them have exited.

Clicking again

While a handler waits, the message loop runs, and so does the button. A second click would start the handler a second time, as with await in C#. Disable the control while the handler runs, as the sample does, or let the handler check a flag. The sample's handler task object is reused for every click. That is safe only because the second click cannot come before the first has ended.

Closing the window

A window that is destroyed while handlers wait leaves those handlers without a window to continue through. Close only once no handler waits:

  • keep the window open in WM_CLOSE while a handler waits, as the sample does, or
  • let the close handler itself be asynchronous: cancel the work, wait for the handler, then close. The window stays open meanwhile. When the handler has ended, ask to close again.

From a worker to the UI

A worker can also run something on the UI thread without anyone waiting: add a task to the dispatcher. AddTask works from any thread, and the task runs on the UI thread as a message of its own:

uiUpdate.Schedule(*ui.dispatcher->Get()); // from a worker task

For frequent progress, do not add a task per step: every task is a posted message, and a thread can have at most 10,000 of them. Count the progress in an atomic variable and read it from a WM_TIMER handler instead.

Message boxes, menus and moving or resizing the window run modal loops of their own. They dispatch WM_DISPATCH like any other message, so waiting handlers go on while a message box is open or the window is being dragged. A handler may show a message box itself, and other handlers continue inside its modal loop.

See also