locked
Trampolining considerations for APM wrappers

    Question

  • Shortly after writing out my last post with an example of translating from Async to APM, I thought that it really should be changed to use TaskContinuationOptions.ExecuteSynchronously.

    And then I thought better of it. I eventually reached a conclusion that TaskContinuationOptions.ExecuteSynchronously should definitely not be used in APM wrappers.

    Please look over my (somewhat random) thoughts below and let me know if I'm totally wrong here.

    One problem with "possibly-asynchronous" callbacks is the different semantics when they execute synchronously (and there's no trampolining system): in extreme cases, this can result in stack exhaustion[1]. This is true for all usually-asynchronous-but-sometimes-synchronous method calls that have completion callbacks (including large portions of the Win32 API).

    APM avoids the problem by exposing IAsyncResult.CompletedSynchronously, which can be checked by AsyncCallback. This allows APM callbacks to implement their own trampolining, which is awkward but doable. However, this isn't a guaranteed solution, because CompletedSynchronously isn't guaranteed to be true, even when the AsyncCallback is invoked from the Begin* method. It does seem to be true for all known implementations, though.

    Except TaskEx.FromResult. The task returned from that method is completed before the method returns, but CompletedSynchronously is hardcoded to false for every task. So it's lying. But it may be OK to lie here, since CompletedSynchronously isn't guaranteed. That does create a problem, though: APM AsyncCallback methods can no longer do trampolining when necessary.

    The solution is to force trampolining when doing a translation from Task to APM. By scheduling a continuation - explicitly without ExecuteSynchronously - the task returned from Begin* trampolines off the actual work task before it completes and invokes AsyncCallback.

    It's slightly inefficient (since it always trampolines, instead of only trampolining when necessary), but it's guaranteed to work. Similarly, the Async CTP always trampolines its continuations if the awaited task says it's not completed. More efficient solutions would require changes (not just additions) to the core TPL[2].

    If I'm right so far, then I'd recommend one of two modifications to the Async CTP:
    A) Document why ExecuteSynchronously should not be used in Async-to-APM wrappers.
    B) Provide general Async-to-APM wrappers. I already have these in one of my company libraries; "public static IAsyncResult ToBegin(Task task, AsyncCallback callback, object state)", "public static void ToEnd(IAsyncResult asyncResult)", and the corresponding <TResult> variants, with the obvious implementations. If they're provided by Async CTP, then we folks won't have a chance to mess them up. :)

          -Steve

    [1] Of course, this only happens in obscure scenarios, but IMO that makes them all the more dangerous (code can pass unit tests, integration tests, and even stress tests without triggering the lurking bug).

    [2] One option is to introduce a TaskContinuationOptions.ExecuteSynchronouslyIfNotCompleted option that allows an atomic if-completed and register-continuation (requiring a change to the Task class). Additionally, one could have TaskEx.FromResult implement CompletedSynchronously correctly (again requiring a change to the Task class), and modify the wrapper to return the inner task directly if it has already completed. However, both of these are fairly complex for a rather minor payoff.


    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
    Thursday, August 04, 2011 8:59 PM

Answers

  • Hi Steve-

    Thanks for the well thought out suggestions.

    Regarding ExecuteSynchronously and APM, that's correct.  Note that for .NET vNext, we actually do have support in TPL for doing the equivalent of ExecuteSynchronouslyIfNotCompleted, but we've not exposed it as an option as such.  Rather, that's the behavior you get through task.GetAwaiter().OnCompleted(...), since it's necessary to support the await model without fear of stack dives but in a high-performance manner.

    Regarding Task.FromResult having its IAsyncResult.CompletedSynchronously return true, we've been debating the right thing to do here.  It's possible before we ship that this could end up returning true, but right now as you say it does return false.

    Thanks, again.

    Wednesday, August 10, 2011 3:57 PM
    Moderator

All replies

  • Hi Steve-

    Thanks for the well thought out suggestions.

    Regarding ExecuteSynchronously and APM, that's correct.  Note that for .NET vNext, we actually do have support in TPL for doing the equivalent of ExecuteSynchronouslyIfNotCompleted, but we've not exposed it as an option as such.  Rather, that's the behavior you get through task.GetAwaiter().OnCompleted(...), since it's necessary to support the await model without fear of stack dives but in a high-performance manner.

    Regarding Task.FromResult having its IAsyncResult.CompletedSynchronously return true, we've been debating the right thing to do here.  It's possible before we ship that this could end up returning true, but right now as you say it does return false.

    Thanks, again.

    Wednesday, August 10, 2011 3:57 PM
    Moderator
  • Hi Steve-

    Thanks for the well thought out suggestions.

    Regarding ExecuteSynchronously and APM, that's correct.  Note that for .NET vNext, we actually do have support in TPL for doing the equivalent of ExecuteSynchronouslyIfNotCompleted, but we've not exposed it as an option as such.  Rather, that's the behavior you get through task.GetAwaiter().OnCompleted(...), since it's necessary to support the await model without fear of stack dives but in a high-performance manner.

    Regarding Task.FromResult having its IAsyncResult.CompletedSynchronously return true, we've been debating the right thing to do here.  It's possible before we ship that this could end up returning true, but right now as you say it does return false.

    Thanks, again.

    Wednesday, August 10, 2011 3:57 PM
    Moderator
  • Stephen,

    You may want to talk to ASP.NET MVC team. If you return a task created with Task.FromResult from async controller the view will never render. I tracked it down to IAsyncResult.CompletedSynchronously. If it returns true that async controller works as expected.

    I also suggest that TaskCompletionSource.TrySetXXX have an overload that accepts completedSynchronoulsy parameter.

    Thursday, April 19, 2012 6:21 PM
  • Stephen,

    You may want to talk to ASP.NET MVC team. If you return a task created with Task.FromResult from async controller the view will never render. I tracked it down to IAsyncResult.CompletedSynchronously. If it returns true that async controller works as expected.

    I also suggest that TaskCompletionSource.TrySetXXX have an overload that accepts completedSynchronoulsy parameter.


    This is a known bug in MVC 4 Beta, and it is being fixed in the next preview release.  See: http://aspnetwebstack.codeplex.com/workitem/22 for more details and a workaround.
    Thursday, April 19, 2012 6:52 PM