locked
Mixing Legacy APM with AsyncCTP

    Question

  • I don't think it's currently possible to write an application that makes use of legacy async-capable classes (such as Stream) and provides a truly async experience (using the AsyncCTP) that supports cancellation. The problem is the disconnect between the legacy APM (Begin/End) that does not provide cancellation of async operations, and the new async/await model in the AsyncCTP which does.

    This is a huge problem if you're trying to build something that reads from Streams but also adheres to cancellation requests. Stephen Toub touched on this disconnect here: http://social.msdn.microsoft.com/Forums/en-US/async/thread/2910aed7-eff7-4770-9eb3-c6213819f1d2/

    It seems to me like a truly async C# library is going to require an entirely new approach to Streams/Readers etc. At the very least they need to be retro-fitted to accept a CancellationToken in each async method. We're contemplating doing this ourselves (CancellableStream?), but that's a little crazy! As most of you know, this async stuff is infectious to an API, and we'd end up having to re-implement every stream/reader implementation we want to use.

    My question is therefore directed towards the AsyncCTP and/or BCL team: Can you provide us with any guidance on how to work around this disconnect? What are the plans going forward?

    It would be great if anyone from the MS-async initiative could share your thoughts (no commitments!! just thoughts!) to help guide us through this transition period.

    Thanks,

    Shawn

    Thursday, June 16, 2011 5:00 PM

Answers

  • I don't work for MS, but I have a ton of asynchronous experience on Windows since 1998.

    Firstly, APM APIs can provide cancellation (and some do); it's just that such support is not standardized. The unified cancellation system did not exist at that time. CTS can be used to convert any older non-standard cancellation system into the unified model.

    That said, APM APIs often do not provide cancellation at all.

    As regarding streams in particular (which are often over a HANDLE), this may be due to historical reasons:

    • The CancelIo function (introduced in Win98/2K) does not cancel a single operation; rather, it cancels all overlapped operations for a specified handle. This doesn't map well to the "cancel a single operation" concept that .NET uses.
      Note: Back in those days, it was very difficult to (correctly) support cancellation on the driver side. These days, the kernel has Cancel-Safe Queue support built-in to XP (back-portable to 2K), and it's a lot easier. So a lot of drivers (especially older drivers) just ignore cancellation anyway (which is legal).
    • The CancelIoEx function does allow cancellation of a single operation, but was introduced with Vista.
      Note: The current .NET version (4.0) still supports XP (SP3), so the BCL cannot easily make use of that API.

    From a higher-level perspective, it's somewhat questionable what benefit you'd get by cancelling a stream operation anyway. You're either in a situation where the results don't matter (e.g., you decide to delete a file you're writing), so finishing writing the current block of data won't make much difference; or you'd have to attempt a cancel and then handle the result differently whether it was cancelled or not (to ensure the data is not corrupted).

    There are some solutions, though. I find that async code is "viral," in the sense that it sort of grows through the code base; but cancel support is not as much.

    A) You can always "prune" the cancel support growth by implementing a cancel stop-gap (for lack of a better term):

    • Wrap your underlying APM in a TCS, similar to the way FromAsync does.
    • If a cancel is requested, cancel your TCS. This allows the cancellation to take effect immediately.
    • When the APM completes, use the TCS.Try methods to issue completion/error.

    This gives the illusion of cancelability; the underlying APM operation will complete, but its results will be ignored. I have a blog entry that describes this same idea applied to asynchronous callbacks. This approach is only feasable if it is correct to ignore errors from cancelled APM operations.

    B) It's also often possible to implement a logical cancel at a higher level of abstraction than a stream. In a client/server project I'm working on now, the client may issue long-running requests to the server. In this case, a "cancellation" of that request sends an actual cancel message to the server. So I'm able to cancel at a higher level of abstraction, and have no requirement for stream-level cancelation.

    C) One final option of note: back in my unmanaged async days, one trick I've used is to close the underlying handle, which causes overlapped operations to complete (with an error). I believe you could do this in .NET as well: close the stream itself, and any Begin* would complete, throwing an IOException from the End* method. You would be limited to cancelling all operations on a stream (like CancelIo), rather than cancelling a single one (like CancelIoEx), so this doesn't map perfectly to the CTS system.

    Good luck!
           -Steve


    Programming blog: http://nitoprograms.blogspot.com/
      Including my TCP/IP .NET Sockets FAQ
      and How to Implement IDisposable and Finalizers: 3 Easy Rules
    Microsoft Certified Professional Developer

    How to get to Heaven according to the Bible
    Friday, June 17, 2011 3:01 PM
  • In addition to Steve's great response, we are planning to integrate I/O cancellation with Task-based async where possible.  As we integrate Task-based async directly into types like FileStream, adding methods like ReadAsync and WriteAsync to Stream directly rather than as extension methods over the APM implementations, we're no longer subject to the restrictions of the current API surface area, and can better integrate cancellation through CancellationToken in a meaningful way.
    Tuesday, July 05, 2011 5:27 PM

All replies

  • I don't work for MS, but I have a ton of asynchronous experience on Windows since 1998.

    Firstly, APM APIs can provide cancellation (and some do); it's just that such support is not standardized. The unified cancellation system did not exist at that time. CTS can be used to convert any older non-standard cancellation system into the unified model.

    That said, APM APIs often do not provide cancellation at all.

    As regarding streams in particular (which are often over a HANDLE), this may be due to historical reasons:

    • The CancelIo function (introduced in Win98/2K) does not cancel a single operation; rather, it cancels all overlapped operations for a specified handle. This doesn't map well to the "cancel a single operation" concept that .NET uses.
      Note: Back in those days, it was very difficult to (correctly) support cancellation on the driver side. These days, the kernel has Cancel-Safe Queue support built-in to XP (back-portable to 2K), and it's a lot easier. So a lot of drivers (especially older drivers) just ignore cancellation anyway (which is legal).
    • The CancelIoEx function does allow cancellation of a single operation, but was introduced with Vista.
      Note: The current .NET version (4.0) still supports XP (SP3), so the BCL cannot easily make use of that API.

    From a higher-level perspective, it's somewhat questionable what benefit you'd get by cancelling a stream operation anyway. You're either in a situation where the results don't matter (e.g., you decide to delete a file you're writing), so finishing writing the current block of data won't make much difference; or you'd have to attempt a cancel and then handle the result differently whether it was cancelled or not (to ensure the data is not corrupted).

    There are some solutions, though. I find that async code is "viral," in the sense that it sort of grows through the code base; but cancel support is not as much.

    A) You can always "prune" the cancel support growth by implementing a cancel stop-gap (for lack of a better term):

    • Wrap your underlying APM in a TCS, similar to the way FromAsync does.
    • If a cancel is requested, cancel your TCS. This allows the cancellation to take effect immediately.
    • When the APM completes, use the TCS.Try methods to issue completion/error.

    This gives the illusion of cancelability; the underlying APM operation will complete, but its results will be ignored. I have a blog entry that describes this same idea applied to asynchronous callbacks. This approach is only feasable if it is correct to ignore errors from cancelled APM operations.

    B) It's also often possible to implement a logical cancel at a higher level of abstraction than a stream. In a client/server project I'm working on now, the client may issue long-running requests to the server. In this case, a "cancellation" of that request sends an actual cancel message to the server. So I'm able to cancel at a higher level of abstraction, and have no requirement for stream-level cancelation.

    C) One final option of note: back in my unmanaged async days, one trick I've used is to close the underlying handle, which causes overlapped operations to complete (with an error). I believe you could do this in .NET as well: close the stream itself, and any Begin* would complete, throwing an IOException from the End* method. You would be limited to cancelling all operations on a stream (like CancelIo), rather than cancelling a single one (like CancelIoEx), so this doesn't map perfectly to the CTS system.

    Good luck!
           -Steve


    Programming blog: http://nitoprograms.blogspot.com/
      Including my TCP/IP .NET Sockets FAQ
      and How to Implement IDisposable and Finalizers: 3 Easy Rules
    Microsoft Certified Professional Developer

    How to get to Heaven according to the Bible
    Friday, June 17, 2011 3:01 PM
  • In addition to Steve's great response, we are planning to integrate I/O cancellation with Task-based async where possible.  As we integrate Task-based async directly into types like FileStream, adding methods like ReadAsync and WriteAsync to Stream directly rather than as extension methods over the APM implementations, we're no longer subject to the restrictions of the current API surface area, and can better integrate cancellation through CancellationToken in a meaningful way.
    Tuesday, July 05, 2011 5:27 PM
  • Thank you Stephens! I'm very happy to hear cancellation will be a first-class citizen in vNext Streams (and hopefully Readers too).

    I ended up writing some Read/Write-Async extensions (as suggested by StephenC) that seem to work ok for now. Having real (and timely) cancellation built-in will be much better though.

    Thanks,

    Shawn

    Thursday, July 07, 2011 1:28 PM
  • ...Stephens...


    Sweet! I'm now associated with someone famous.

    :)

           -Steve


    Programming blog: http://nitoprograms.blogspot.com/
      Including my TCP/IP .NET Sockets FAQ
      and How to Implement IDisposable and Finalizers: 3 Easy Rules
    Microsoft Certified Professional Developer

    How to get to Heaven according to the Bible
    Friday, July 08, 2011 8:38 PM
  • Hehe :)

    Saturday, July 09, 2011 12:05 AM