Asynchronous ReadDirectoryChangesW call blocks the thread from exiting
Master System Design with Codemia
Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.
Introduction
ReadDirectoryChangesW can be issued asynchronously, but that does not mean shutdown becomes automatic. If a thread starts a directory watch and then exits without canceling the outstanding request, the program often appears to "hang" because some part of the shutdown path is still waiting for that I/O to finish.
What Asynchronous Means Here
On Windows, ReadDirectoryChangesW becomes asynchronous when the directory handle is opened with FILE_FLAG_OVERLAPPED and the call receives an OVERLAPPED structure. The function then starts a pending request and returns before any file-system event arrives.
That behavior is useful for long-running watchers because directory monitoring is naturally event-driven. The important detail is that the watch request is still active inside the kernel. If your code later waits for the worker thread, waits on the overlapped event forever, or closes down shared resources in the wrong order, the thread can look blocked even though the original API call was "async".
Why Thread Exit Gets Stuck
The usual failure mode is:
- a worker thread starts
ReadDirectoryChangesW - the request remains pending because no file change has happened yet
- the application asks the worker thread to stop
- the worker thread is still waiting for completion or the main thread is waiting for the worker thread
In other words, the problem is not that ReadDirectoryChangesW secretly became synchronous. The problem is that pending asynchronous I/O must be canceled or completed before the shutdown sequence can finish cleanly.
If you use completion routines instead of an event, there is another trap: completion routines run only when the issuing thread enters an alertable wait such as SleepEx or WaitForSingleObjectEx with the alertable flag. Without that, completion may never be observed by your shutdown logic.
A Safer Watcher Pattern
The simplest reliable pattern is:
- open the directory handle for overlapped I/O
- associate an event with the
OVERLAPPEDstructure - keep a separate stop event
- on shutdown, signal stop and call
CancelIoEx - wait for the worker to observe cancellation and exit
This code uses an explicit cancellation path. That is the critical difference between a watcher that stops predictably and one that keeps shutdown stuck.
Handle Closure and Cancellation
Many developers assume closing the thread or closing the handle will always make the problem disappear. That is too optimistic. You want shutdown to be deliberate:
- request stop
- cancel pending I/O with
CancelIoEx - observe completion or cancellation result
- close the handle only after the request is no longer active
CancelIoEx is especially useful because it can cancel I/O issued by any thread, while CancelIo is limited to operations started by the calling thread.
Completion Routine Variant
If you use a completion routine instead of an event, your thread needs an alertable wait:
Without an alertable wait, the completion callback will not run, which often gets misdiagnosed as the API "blocking" thread exit.
Common Pitfalls
The most common mistake is starting overlapped directory monitoring and never implementing a cancellation path. The second is forgetting that completion routines need alertable waits.
Another issue is waiting for the worker thread before canceling its pending I/O. That reverses the shutdown order and creates a deadlock-like stall. Developers also sometimes reuse the same OVERLAPPED structure incorrectly across active operations, which leads to confusing results and hard-to-reproduce failures.
Summary
- '
ReadDirectoryChangesWcan be asynchronous without making shutdown automatic.' - A pending watch request must be canceled or completed before the thread can exit cleanly.
- '
CancelIoExis the usual tool for stopping an outstanding directory watch.' - Completion routines require an alertable wait such as
SleepExorWaitForSingleObjectEx. - Clean shutdown depends on ordering: signal stop, cancel I/O, observe completion, then close handles.

