AsyncEx icon indicating copy to clipboard operation
AsyncEx copied to clipboard

Handle completed tasks in ApmAsyncFactory.

Open fiseni opened this issue 1 year ago • 5 comments

This fixes #281.

I couldn't really find documentation on how to handle sync operations in APM. I tested various options, and it seems the IAsyncResult response should have the CompletedSynchronously set to true to indicate that this is a sync flow. In that case, we're not obliged to call the callback, and that's exactly what we need. But, the Task has a hard-coded false value for this property, so returning Task will never work in this scenario. I just ended up using a new internal type CompletedAsyncResult for this scenario.

This fixes all issues, at least the usages I can think of.

fiseni avatar Oct 20 '23 19:10 fiseni

CompletedSynchronously was my first thought as well. However, that cannot be the whole story because the alternative solution also uses a Task implementation of IAsyncResult, which has CompletedSynchronously set to false.

StephenCleary avatar Oct 21 '23 19:10 StephenCleary

For ToBegin

  • If the task is already completed, we're returning the newly defined IAsyncResult implementation CompletedAsyncResult, which has set the CompletedSynchronously to true.
  • If the task is not completed, the flow remains as it is. The ToBegin returns (since we have async void) and the callback will be invoked once the original task is completed.

For ToEnd

  • If the IAsyncResult is CompletedAsyncResult then we know it was a sync flow, and we're returning the Result.
  • If the IAsyncResult is Task then we unwrap and return the result.

What am I missing here? 🤔

fiseni avatar Oct 21 '23 21:10 fiseni

I might have misunderstood your comment. What do you mean by "alternative solution"? Are you referring to the solution in this article? It's using a continuation, which is dispatched in a separate thread. That's why it always works and we don't need the sync flow (even if the task returns synchronously).

public static IAsyncResult AsApm<T>(this Task<T> task,
                                    AsyncCallback callback,
                                    object state)
{
    if (task == null)
        throw new ArgumentNullException("task");

    var tcs = new TaskCompletionSource<T>(state);
    task.ContinueWith(t =>
                      {
                         if (t.IsFaulted)
                            tcs.TrySetException(t.Exception.InnerExceptions);
                         else if (t.IsCanceled)
                            tcs.TrySetCanceled();
                         else
                            tcs.TrySetResult(t.Result);

                         if (callback != null)
                            callback(tcs.Task);
                      }, TaskScheduler.Default);
    return tcs.Task;
}

fiseni avatar Oct 22 '23 11:10 fiseni

I saw you already found a solution. So please do not feel pressured in any way by this comment. It just happened, that i stumbled apon an other solution for this problem earlier this day. You may wanna have a look at this Task to apm wrapper.

The difference is not that large. It also uses an async result wrapper. It might be a little more lightweight, thatn allocation a new task completion source, but i cannot judge if thats a noticable difference (i highly doubt it).

Pretasoc avatar Jun 26 '24 15:06 Pretasoc

Thanks. I'm just a contributor and created the PR only as a suggestion. @StephenCleary will review it at some convenient time. Perhaps he has other ideas for resolving the issue at hand.

fiseni avatar Jul 08 '24 14:07 fiseni