Why doesn’t my asynchronous read operation complete when I close the handle?

Raymond

A customer was using asynchronous I/O with an I/O completion port. At the time they wanted to shut things down, they still had an outstanding asynchronous read. To get things to clean up, they closed the file handle, expecting it to cause the Read­File to complete and fail with an error like ERROR_INVALID_HANDLE. But instead, what they found was that the read operation remained outstanding, and nothing completed.

What’s going on?

What’s going on is that when you close the file handle, that decrements an internal reference count on the underlying file object, and that internal reference count is not yet zero, so the file is still open. And where did that extra reference count come from?

From the Read­File operation itself!

In the kernel, one of the things that happens when you pass a handle from an application is that the kernel validates the handle and obtains a reference-counted pointer to the underlying kernel object, which temporarily bumps the object reference count by one. If you close the handle, that drops it back down, but there is still the outstanding reference from the I/O operation, and that outstanding reference won’t go away until the I/O operation completes.

As a side note, closing the handle to an object while there is still outstanding work on that object feels really sketchy to me. It’s getting dangerously close to “destroying an object while simultaneously using it”.

What you need to do is cancel the I/O by calling a function like Cancel­Io or Cancel­Io­Ex. And in order to cancel the I/O, you need to proide a handle to the file whose I/O you want to cancel.

Another reason not to close that handle yet.

When you cancel the I/O, the I/O will complete with an error code saying that it was cancelled. At that point, you can close the file handle and clean up.

4 comments

Comments are closed. Login to edit/delete your existing comments

  • switchdesktopwithfade@hotmail.com

    I can’t imagine the hell of working with async I/O without coroutines or stream class wrappers. You ending up forgetting (or becoming too terrified to implement) at least half of the obvious boilerplate like ref-counting your operations. Plus if I’m not mistaken you have to maintain your own safeguarded seek pointer because file reads are specified as absolute offsets. Lately the threadphobes are talking wildly about IORING being the cure-all and I’m just SMH.

    p.s. Why no async CreateFile or GetFileAttributes yet?

  • Sebastiaan Dammann

    I can see someone ignorant calling CloseHandle twice as a workaround. Or does the kernel maintain two reference counts?

    • Raymond ChenMicrosoft employee

      Double-closing a handle just fails the second close operation with ERROR_INVALID_HANDLE. Kernel objects are reference-counted, but handles aren’t. One way of imagining it is that the kernel object reference count is the number of outstanding handles, and when I/O begins, the app-provided handle is duplicated and the duplicate is closed when the I/O completes. (But internally, the handle itself is optimized out and the kernel just manipulates the the internal reference count directly, so the internal reference count is “the number of outstanding handles + the number of outstanding I/O operations”.)

  • Kalle Niemitalo

    !object shows both “HandleCount” and “PointerCount”. According to the WDK documentation, when the last handle is closed, that triggers an IRP_MJ_CLEANUP request, and the driver must then complete the I/O requests as cancelled. However, the IRP_MJ_CLEANUP (IFS) topic does not mention the same requirement. Is that then a difference between file system drivers and other drivers? I suppose that, if the Cache Manager has its own pending I/O requests on the same file object, it does not expect the file system driver to cancel those during IRP_MJ_CLEANUP.