December 21st, 2022

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

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.
Topics
Code

Author

Raymond has been involved in the evolution of Windows for more than 30 years. In 2003, he began a Web site known as The Old New Thing which has grown in popularity far beyond his wildest imagination, a development which still gives him the heebie-jeebies. The Web site spawned a book, coincidentally also titled The Old New Thing (Addison Wesley 2007). He occasionally appears on the Windows Dev Docs Twitter account to tell stories which convey no useful information.

3 comments

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

  • Ian Boyd

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

    Read more
    • Raymond ChenMicrosoft employee Author · Edited

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

  • MGetz

    Odd thing to notice: is more error prone than . Not sure why this stood out to me but it did. I'm guessing it was because the version doesn't require using * 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 requires atomicity of some form, so even using it wrong is...

    Read more