5

I was wondering which approach would be more efficient in terms of memory and resource usage in general.

Particular with approach #1, I'm having a hard time visualizing how the task objects will be created and threads spun up? Could someone please explain what goes on under the covers in detail as an aside?

I'd like to use #1 if there's no difference between the two (want to avoid bubbling up async). With #2, I understand the compiler will generate a state machine underneath and yield return. OTOH, #1 seems recursive conceptually but will it be recursive in the traditional sense as in one stack-frame waiting on the other?

Approach #1:

internal static Task ExecuteAsyncWithRetry(Func<Task> methodToExecute, Func<bool> shouldRetry)
    {
        var tcs = new TaskCompletionSource<object>();

        try
        {
            return methodToExecute().ContinueWith<Task>((t) =>
            {
                if (t.IsFaulted || t.IsCanceled)
                {
                    if (shouldRetry())
                    {
                        return ExecuteAsyncWithRetry(methodToExecute, shouldRetry);
                    }
                    else
                    {
                        tcs.SetException(t.Exception);
                    }
                }
                else
                {
                    tcs.SetResult(null);

                }

                return tcs.Task;
            }, TaskContinuationOptions.ExecuteSynchronously).Unwrap();
        }
        catch(Exception ex)
        {
            tcs.SetException(ex);
        }

        return tcs.Task;
    }

Approach #2 (ignore the difference in exception propagation between the two):

internal static async Task ExecuteWithRetry(Func<Task> methodToExecute, Func<bool> shouldRetry)
    {
        while (true)
        {
            try
            {
                await methodToExecute();
            }
            catch(Exception ex)
            {
                if(!shouldRetry())
                {
                    throw;
                }
            }
        }
    }
Kakira
  • 846
  • 1
  • 8
  • 14
  • The 2nd approach is clearer to read and understand. My rule of thumb: use async-await if possible. And in your case it is possible. But you are missing some kind of termination condition in the 2nd case. If everything goes fine, `methodToExecute()` will be executed over and over again. – Krumelur Mar 30 '14 at 10:22
  • Note, that await loses error information because it unwraps AggregateException's. That's undesirable for a generic helper method like this one. It is not in the business of interpreting exceptions. – usr Mar 30 '14 at 13:21
  • Thanks, @usr, however, could you clarify on losing the error information part? Shouldn't await behave like UnWrap() and propagate the error/exception of the inner task? – Kakira Mar 30 '14 at 19:58
  • The default awaiter discards all but the first inner exception. – usr Mar 30 '14 at 20:21

1 Answers1

5

Besides different exceptions and cancellation propagation, there's another major difference.

In the 1st case, your continuation runs on the same thread where the task has completed, because of TaskContinuationOptions.ExecuteSynchronously.

In the 2nd case, it will be run on the original synchronization context (if methodToExecute was called on a thread with synchronization context).

Even though the 1st approach might be more efficient, it also might be hard to understand (especially when your or someone else returns to it in a year from now).

I'd follow the KISS principle and stick with the second one, with one amendment:

await methodToExecute().ConfigureAwait(false);

Updated to address the comment:

"OTOH, #1 seems recursive conceptually but will it be recursive in the traditional sense as in one stack-frame waiting on the other?"

For #1, whether it will happen recursively on the same stack frame, or asynchronously on a different stack frame, totally depends on what's going inside methodToExecute. In most cases, there will be no traditional recursion, if you use some naturally async APIs inside methodToExecute. E.g., HttpClient.GetStringAsync completes on a random IOCP pool thread, and Task.Delay completes on a random worker pool thread.

However, even an async API may complete synchronously on the same thread (e.g. MemoryStream.ReadAsync or Task.Delay(0)), in which case there will be recursion.

Or, using TaskCompletionSource.SetResult inside methodToExecute may also trigger a synchronous continuation.

If you really want to avoid any possibility of recursion, call methodToExecute via Task.Run (or Task.Factory.StartNew / Task.Unwrap). Or, better yet, remove TaskContinuationOptions.ExecuteSynchronously.

For #2, the same scenario is also possible, even when there's a synchronization context on the initial thread.

Community
  • 1
  • 1
noseratio
  • 59,932
  • 34
  • 208
  • 486
  • Sure, thanks for the answer and I did know about their differences. However, I was hoping for an answer past these nuances for my personal understanding/solidification of concepts. Especially with #1, I'd like to know what's going on under the covers. – Kakira Mar 29 '14 at 00:47
  • @Kakira, now sure I follow. If you've come up with this code yourself, you should probably understand how it works. If not, what part do you not understand, specifically? FYI, a [related question](http://stackoverflow.com/q/21345673/1768303). – noseratio Mar 29 '14 at 00:51
  • Like I said (and don't worry, this is my code), I'm trying to visualize what's going on under the covers. I'm specifically trying to understand this part of my question: "OTOH, #1 seems recursive conceptually but will it be recursive in the traditional sense as in one stack-frame waiting on the other?" – Kakira Mar 29 '14 at 01:00