When doing system-related programming, you sometimes need to receive notifications of system events. Such situations include:


  • another process has quit
  • you want your application get notified it should quit
  • the terminal server is ready


In this article I’m presenting a handy C++ class that solves a common requirement: using the system thread pool in order to get notified once a handle becomes signaled, asynchronously and thread-safe.


The Problem


The task of watching a handle to become signaled is performed in a different thread of execution, because you don’t want to block the main flow of the program. A quick and dummy approach is creating a thread that waits for a specific handle and then quits. But then you realise you have spun up some threads that do nothing else than occupying system resources and sit around waiting. You probably also had to create yet another thread class just for this very purpose. And deep inside you feel that there must be a better and de facto standard way, saving you from reinventing the wheel once again.


Indeed, since Windows XP/Windows Server 2003 there is a system thread pool that was invented exactly for this purpose (and others), right at your hands via the RegisterWaitForSingleObject and its clean-up counter-part UnregisterWait[Ex]; with the advantage that:


  • it saves you from writing yet another thread
  • it is much more light-weight and resource-friendly by utilising a thread pool
  • you can leverage an existing asynchronous notification system


That sounds simple, but given the whole bunch of possibilities the API is offering, quite the opposite is true. On one hand it’s the way to use the API to achieve what you want. On the other hand you are not exempt from the caveats of thread-safe coding when it comes to installing and cleaning up the event listener. Even as a seasoned and accurate programmer, I had to read the documentation multiple times and investigate thoroughly how other people were using it.


One-Shot handle_watcher


There are enough blogs and mentions about those API calls out there on the net. The unique thing to my post is where the handle watcher comes into play: you only need to pass it a handle and a callback and make sure the callback function is thread-safe and its implementation non-blocking. As a usual bonus when encapsulating dedicated problems, your code becomes spaghetti-free and worry-free.


Note: RegisterWaitForSingleObject and the system thread pool open up some powerful possibilities, e.g. periodic notifications. Here we are concentrating only on listening to a handle’s signaled state once, and that’s the only purpose of the handle watcher class.


Now, the core of the class is quite straightforward:


// Smart pointer closing the handle with a call to CloseHandle
typedef std::unique_ptr<void, int(__stdcall*)(void*)> handle_ptr;

/** @short Watches a given handle once until it becomes signalled, whereupon the callback (work item) gets called. */
class handle_watcher
{
public:
    /** @short Take ownership of the handle and register it to be waited on in the system thread pool.
        The callback (work item) is executed once the handle becomes signalled.
        it is reset as soon as it has been executed. If you have special bound parameters, ensure
        that destroying them is thread-safe.
        @return Whether the passed-in handle is valid
     */
    bool watch_this(handle_ptr handle, std::function<void()> workItem);
    bool watch_this(void*&& handle, std::function<void()> workItem);

    /** @short Unregisters conditionally (waiting for already queued callbacks to get called) from the system thread pool
        @return The returned handle comes in handy when you still need to use it for post-flight operations;
        it is valid only when the callback wasn't called before, otherwise, it is null
     */
    handle_ptr cleanup();

private:
    // internal callback handler
    void signaled();
};


The way it works is as follows:


watch_this takes the handle (correct: taking ownership) to get registered in the thread pool and a callback function (which is also called a work item on MSDN). The callback is registered to be called exactly once and not every time the handle would become signalled.


Now that being said you can figure what the private method signalled is up to it is registered as an internal signal handler. When it gets called it invokes the callback you provided to watch_this, then resets the handle and unregisters it from the thread pool, unconditionally, meaning it doesn’t wait for any queued callbacks. This might be logical but it is an important detail when calling UnregisterWaitEx - the callback (signalled itself or the callback you passed to watch_this must be non-blocking).


signalled also resets the callback as soon as it has been executed - a measure of freeing resources early, which makes sense to me because the handle watcher usually lives during the whole application uptime. Because the handle watcher gets notified from any thread out of the thread pool, one has to ensure that destroying possibly bound parameters is thread-safe.


cleanup is used when you stop watching the handle upon program exit. It unregisters the handle from the thread pool but waits until any already-queued callbacks have been called. This is another reason why the callback itself must be non-blocking, which in turn means it must not be called from your provided callback! cleanup is a no-op if the watcher has been notified - signalled has already performed the necessary cleanup.


In case the handle didn’t become signalled and the callback wasn’t called, cleanup releases the handle and returns it to the caller. The handle can then be used further, e.g. for quitting the associated application. In case the handle became signalled and the callback has been invoked, the returned handle is simply null.


Of course, all of the operations are performed in a thread-safe way. The only thing you need to ensure is that your callback function, the work item, is thread-safe and non-blocking as well, otherwise, the notifying thread cannot return to the pool or cleanup might dead-lock. The best practice is to asynchronously notify the thread that installed the watch, which gives you the simplicity of single-threaded processing.


Usage Example


Let’s take a look at how you would ensure an application you started from your program keeps running. Let’s assume you have the following WTL window:


#include <windows.h>
#include <atlbase.h>
#include <atlwin.h>
#include <atlmisc.h>
#include <atlcrack.h>
#include <atlapp.h>
#include "handle_watcher.h"

class MyWindow:
    public CWindowImpl<MyWindow, CWindow, CWinTraits<WS_POPUP | WS_CLIPSIBLINGS | WS_CLIPCHILDREN>>
{
private:
    BEGIN_MSG_MAP(MyWindow)
        MSG_WM_CREATE(OnCreate)
        MSG_WM_DESTROY(OnDestroy)
        MSG_WM_CLOSE(OnClose)
        MESSAGE_HANDLER_EX(ProcessTerminated, OnProcessTerminated)
    END_MSG_MAP()

private:
    virtual void OnFinalMessage(HWND thisWnd) override;

private:
    int OnCreate(LPCREATESTRUCT createStruct);
    void OnClose();
    void OnDestroy();
    
    LRESULT OnProcessTerminated(UINT msgCode, WPARAM wParam, LPARAM lParam);

    void StartMyProcess();

public:
    enum UserMessages: UINT
    {
        ProcessTerminated = WM_USER,
    };

private:
    handle_watcher processWatcher_;
};


void MyWindow::OnFinalMessage(HWND thisWnd)
{
    if (handle_ptr processHandle = processWatcher_.cleanup())
    {
        // quit my app
    }
}

int MyWindow::OnCreate(LPCREATESTRUCT)
{
    StartMyProcess();

    return true;
}

void MyWindow::OnDestroy()
{
    PostQuitMessage(0);
}

void MyWindow::OnClose()
{
    DestroyWindow();
}

LRESULT MyWindow::OnProcessTerminated(UINT, WPARAM, LPARAM)
{
    StartMyProcess();

    return true;
}

void MyWindow::StartMyProcess()
{
    processWatcher_.watch_this(start_process(L"myapp.exe"), [this]()
    {
        // might get called any time, retrieve hWnd once and check
        if (CWindow wnd = m_hWnd)
            wnd.PostMessage(ProcessTerminated);
    };
}


All set? Attached you will find the whole implementation of the handle watcher. Have fun and keep the copyright!