Running some UI code on a timer at a higher priority than your usual timer messages, or without coalescing

Raymond Chen

A customer wanted something similar to a Windows WM_TIMER timer, but they didn’t want the timer to be a low-priority message generated on demand, and they didn’t want the messages to be coalesced. Is this possible?

You can do this by using some other kind of timer, but using window messages to synchronize with the UI thread. In other words, treat this as a work queue where the work is generated by a non-UI timer.

A naïve solution would be to post a custom message each time the timer elapses:

HWND g_hwnd;
PTP_TIMER g_timer = nullptr;

void CALLBACK TimerCallback(PTP_CALLBACK_INSTANCE instance,
    void* context, PTP_TIMER timer)
{
    PostMessage(g_hwnd, WM_DOSOMETHING, 0, 0);
}

// Must be called from UI thread
void StartTimer(
    FILETIME dueTime, DWORD period, DWORD window)
{
    g_timer = CreateThreadpoolTimer(TimerCallback,
        nullptr, nullptr);
    SetThreadpoolTimer(
        g_timer, &dueTime, period, window);
}

// Must be called from UI thread
void StopTimer()
{
    CloseThreadpoolTimer(g_timer);
}

LRESULT CALLBACK WndProc(HWND hwnd, UINT message,
    WPARAM wParam, LPARAM lParam)
{
    switch (message)
    {
    ...
    case WM_DOSOMETHING:
        DoSomething();
        break;
    ...
    }
    return DefWindowProc(hwnd, message, wParam, lParam);
}

It’s very simple: Each time the timer fires, we post a “do something” message to the window.

The problem with this is that it can flood the message queue if the UI thread gets blocked for a long time for some reason. The timer keeps running, and new WM_DO­SOMETHING messages are posted into the queue, but the UI thread has stopped processing messages, so the queued messages just accumulate, and you risk overflowing the message queue.

Better is to use an edge-triggered message, using a technique we saw some time ago.

HWND g_hwnd;
PTP_TIMER g_timer = nullptr;

// alternate: LONG g_count;
std::atomic<int> g_count;

void CALLBACK TimerCallback(PTP_CALLBACK_INSTANCE instance,
    void* context, PTP_TIMER timer)
{
    // alternate: InterlockedIncrement(&g_count)
    if (g_count.fetch_add(1,
            std::memory_order_relaxed) == 1) {
        PostMessage(g_hwnd, WM_DOSOMETHING, 0, 0);
    }
}

LRESULT CALLBACK WndProc(HWND hwnd, UINT message,
    WPARAM wParam, LPARAM lParam)
{
    switch (message)
    {
    ...

    case WM_DOSOMETHING:
        {
            // alternate: InterlockedExchange(&g_count, 0)
            int count = g_count.exchange(0,
                std::memory_order_relaxed);
            for (int i = 0; i < count; i++) DoSomething();
        }
        break;
    ...
    }
    return DefWindowProc(hwnd, message, wParam, lParam);
}

This time, when the timer expires, we just increment the number of outstanding operations. If the number incremented from zero to one, then we post a message to get the UI thread to drain the work.

When the UI thread receives the message, it exchanges the counter back to zero and then operates on each of the operations that were outstanding. In this simple example, we just do the “something” that many times. In a real program, you would probably write a special version DoSomethingN(count) which is more efficient than doing DoSomething() N times.

Note that I’ve been glossing over the problem of what to do if StopTimer() is called while there are still pending operations. As-written, what happens is that the pending operations will still be performed later. If you want them to be flushed or recalled, you have a little more work to do. I’ll leave you work out the details of threadpool timers, but the idea is

  • Stop the timer and wait for callbacks to complete.
  • Exchange the g_count back to zero.
  • If flushing: Perform DoSomething() that many times immediately.

3 comments

Discussion is closed. Login to edit/delete existing comments.

  • MGetz 0

    Odd thing to notice: LONG g_count; is more error prone than std::atomic g_count;. Not sure why this stood out to me but it did. I’m guessing it was because the LONG version doesn’t require using Interlocked* methods. So someone not in on the fact that it needs to be atomic could directly use it if they forgot it needed to be treated carefully. Whereas the std::atomic requires atomicity of some form, so even using it wrong is less likely because it lets the following programmer know “Hey this is something you should be careful with”. Obviously they could still cause undefined behavior but given that they are more likely to default to sequential consistency, which while slower would still work without causing undefined behavior.

  • Ian Boyd 1

    It also doesn’t deal with the issue of:

    – the background thread posts a message on the rising edge to the main thead
    – the main thread receives the message, and exchanges the counter back to zero, and does the work
    – in the meantime, the background thread causes another rising edge, and posts a message
    – the main thread completes its work, and goes back to get the next message from the queue
    – it’s a WM_DoSomething message, which causes it exchange the counter back to zero and do work
    – the main thread completes its work, and goes back to get the next message from the queue
    – it’s a WM_DoSomething message, which it does
    – and then it gets another WM_DoSomething
    – and then it gets another WM_DoSomething
    – and then it gets another WM_DoSomething
    – and then it gets another WM_DoSomething
    – and then it gets another WM_DoSomething
    – and then it gets another WM_DoSomething

    And now your multi-threaded application is locked up, not processing any paint messages, etc.

    I learned that the hard way in 2006 with my first dual-code machine. The code that was nicely multi-threaded suddently devolves to being single-threaded, because the main thread is stuck consuming work produced by the background thread.

    The solution **is** to use a timer, because timer messages are lower priority than paint messages, input messages, etc.

    • Raymond ChenMicrosoft employee 1

      True, but the premise of the question is that the behavior you describe is what they wanted.

Feedback usabilla icon