Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Retrying async functions using a task -- what approach is more efficient?

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;
                }
            }
        }
    }
like image 866
Kakira Avatar asked Mar 28 '14 23:03

Kakira


People also ask

How does async improve performance?

Asynchronous programming can in some cases help with performance by parallelizing a task. But, that is not its main benefit in day to day development. Instead, the main benefit comes from making our code more scalable. The scalability feature of a system relates to how it handles a growing amount of work.

What is the advantages of async await approach?

The main benefits of asynchronous programming using async / await include the following: Increase the performance and responsiveness of your application, particularly when you have long-running operations that do not require to block the execution.

Which is the recommended way to wait for an async method to complete?

No problem, just make a loop and call this function with an await: [code] for (int i = pendingList. Count - 1; i >= 0; i--)

Should async method return Task?

For methods other than event handlers that don't return a value, you should return a Task instead, because an async method that returns void can't be awaited. Any caller of such a method must continue to completion without waiting for the called async method to finish.


1 Answers

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.

like image 110
noseratio Avatar answered Sep 26 '22 05:09

noseratio