Windows CancelSynchronousIo: Cancel the Right Thread, Then Observe Completion
Use CancelSynchronousIo for blocked Windows synchronous I/O while controlling thread ownership, cancellation races, and final operation results.
CancelSynchronousIo is a thread-targeted request, not a magic stop button for a file handle or an entire workflow. It marks pending synchronous I/O issued by the specified thread for cancellation. That distinction matters when a worker performs several blocking calls in sequence: by the time a controller requests cancellation, the call it intended to stop may have completed and the thread may be inside another operation.
Understand the three cancellation scopes
Microsoft documents three related APIs with different targets. CancelIo concerns outstanding overlapped I/O issued by the calling thread on a given handle. CancelIoEx addresses outstanding I/O on a specified file handle, including requests issued by other threads in the process. CancelSynchronousIo instead takes a thread handle and attempts to cancel that thread’s current synchronous I/O. Do not substitute one for another solely because all three have “Cancel” in the name.
The caller of CancelSynchronousIo needs a thread handle with THREAD_TERMINATE access. The function returns nonzero when the cancellation request succeeds. If no request is found, it returns zero and GetLastError() reports ERROR_NOT_FOUND. This can mean the operation finished before the request arrived; it is not proof that the worker’s larger job succeeded.
The key selector is the thread, not a file path, handle, or individual request ID. If that thread can be inside multiple different synchronous APIs at different times, the controller cannot know which call will be current merely by remembering which one it expected to be current. Microsoft explicitly describes a race in a series of calls: a late cancellation intended for X can affect the next call Y. The cancellation protocol therefore needs application-level state around every operation boundary.
Do not mistake THREAD_TERMINATE access for an instruction to terminate the thread. The function’s documented requirement is an access right on the handle; its purpose is to mark pending synchronous I/O for cancellation. Forceful thread termination would bypass cleanup and is a different, hazardous operation.
Use a worker with a clear owner
A robust design keeps each cancellable synchronous operation on a dedicated worker whose lifetime and thread handle the controller owns. The controller records a stop request in application state, calls CancelSynchronousIo if the worker may be blocked, and then waits for the worker to report its actual result. The worker checks the stop state before starting each subsequent I/O call. This closes the gap where cancellation arrives between calls and the worker otherwise proceeds into the next one.
A useful state machine has idle, running, stop requested, and finished states guarded by normal synchronization. The controller moves to stop requested before making the cancellation call. The worker checks this state before each blocking operation and reports its final state only after the API returns. If the controller receives ERROR_NOT_FOUND, it still waits for the worker’s report, because the operation may have completed normally or the worker may be between calls. Reusing the same worker for a new job requires a new, explicit generation of work so a late stop request is not applied to it.
Keep the thread handle valid until the controller has finished its cancellation and join protocol. Closing it early makes the intended target unavailable. Likewise, the worker’s I/O buffer and any object referenced by completion handling must remain valid until the blocking call has returned. A cancellation request does not release those ownership obligations.
Avoid targeting a shared thread-pool worker. Microsoft specifically warns that the thread could switch from operation X to operation Y before the cancellation request reaches it. The request can then interrupt the wrong work. If the design needs pooled workers, consider an overlapped I/O model with explicit per-operation state instead of using a thread-wide cancellation target.
Cancellation is not completion
The API marks requests; it does not promise every driver will cancel them promptly. Microsoft’s cancellation guide warns that driver support can vary. The worker must observe the return from the blocking API and classify success, cancellation, or another failure. Do not free its buffers, close shared state, or reuse its thread identity until the worker has actually exited the operation and the controller has synchronized with it.
This is a bounded-shutdown design problem. If the underlying driver does not complete cancellation, waiting forever may be unacceptable for an interactive application, yet killing the thread is not a safe general remedy. Define a timeout for the controller’s wait and a degraded-state behavior, such as reporting that shutdown is still pending or isolating the worker behind a process boundary where a larger recovery plan exists. Do not report successful cancellation merely because a timeout fired; preserve enough diagnostic information to identify the device, API, and thread state without logging sensitive payloads.
Not every Windows file operation supports these cancellation APIs. Microsoft lists CopyFile, MoveFile, MoveFileEx, and ReplaceFile as examples that cannot be canceled this way, and points to alternate APIs such as CopyFileEx or MoveFileWithProgress where appropriate. Verify the actual blocking call before choosing a cancellation strategy.
Test with a controlled slow device or pipe, a request that finishes just before cancellation, a stop request between two I/O calls, and an uncooperative driver. Record both the cancellation API result and the worker’s final result; a log line saying “cancel requested” must never be presented as “operation canceled.”
When reviewing telemetry, distinguish ERROR_NOT_FOUND from a failed permission check or an unexpected invalid handle. The former can be a normal race with completion; the latter points to a broken ownership protocol. Also distinguish “API returned after cancellation” from “the whole job rolled back.” A synchronous write may have partially completed before it was interrupted, so higher-level code must validate or repair application state according to that operation’s contract. Cancellation is control flow, not a transaction.
Related:
- CancelIoEx: Correct Cancellation and Completion for Windows Overlapped I/O
- Windows Thread-Pool I/O: Pair Overlapped Requests With Completion Callbacks
Sources: