Search code examples
c#performanceasync-awaittheory

async await performance?


(Just a theoretical question - for non-gui apps)

Assuming I have this code with many awaits:

public async Task<T> ConsumeAsync()
    {
          await A();
          await b();
          await c();
          await d();
          //..
    }

Where each task can take a very short period of time ,

Question (again , theoretical)

There could be a situation where the overall time dealing with all those "releasing back threads" and "fetching threads back" ( red & green here :)

enter image description here

Is taking more time than a single thread which could done all the work with a small amount of delay ,

I mean , I wanted to be the most productive , but instead , since all those switches back and forth - I actually lost productivity.

Can such scenario occur ?


Solution

  • A Task object represent the deferred result of a pending operation. You don't have to use tasks and async/await if you don't have any pending operations. Otherwise, I believe async/await code is generally more efficient than its bare TPL ContinueWith analogue.

    Let's do some timing:

    using System;
    using System.Threading;
    using System.Threading.Tasks;
    
    namespace ConsoleApplication
    {
        class Program
        {
            // async/await version
            static async Task<int> Test1Async(Task<int> task)
            {
                return await task;
            }
    
            // TPL version
            static Task<int> Test2Async(Task<int> task)
            {
                return task.ContinueWith(
                    t => t.Result,
                    CancellationToken.None,
                    TaskContinuationOptions.ExecuteSynchronously,
                    TaskScheduler.Default);
            }
    
            static void Tester(string name, Func<Task<int>, Task<int>> func)
            {
                var sw = new System.Diagnostics.Stopwatch();
                sw.Start();
                for (int i = 0; i < 10000000; i++)
                {
                    func(Task.FromResult(0)).Wait();
                }
                sw.Stop();
                Console.WriteLine("{0}: {1}ms", name, sw.ElapsedMilliseconds);
            }
    
            static void Main(string[] args)
            {
                Tester("Test1Async", Test1Async);
                Tester("Test2Async", Test2Async);
            }
        }
    }
    

    The output:

    Test1Async: 1582ms
    Test2Async: 4975ms
    

    So, by default, await continuations are handled more efficiently than ContinueWith continuations. Let's optimize this code slightly:

    // async/await version
    static async Task<int> Test1Async(Task<int> task)
    {
        if (task.IsCompleted)
            return task.Result;
        return await task;
    }
    
    // TPL version
    static Task<int> Test2Async(Task<int> task)
    {
        if (task.IsCompleted)
            return Task.FromResult(task.Result);
    
        return task.ContinueWith(
            t => t.Result,
            CancellationToken.None,
            TaskContinuationOptions.ExecuteSynchronously,
            TaskScheduler.Default);
    }
    

    The output:

    Test1Async: 1557ms
    Test2Async: 429ms
    

    Now the non-async version wins. In case with the async version, I believe this optimization has already been done internally by the async/await infrastructure.

    Anyway, so far we've dealt only with completed tasks (Task.FromResult). Let's introduce the actual asynchrony (naturally, we'll do less iterations this time):

    static Task<int> DoAsync()
    {
        var tcs = new TaskCompletionSource<int>();
        ThreadPool.QueueUserWorkItem(_ => tcs.SetResult(0));
        return tcs.Task;
    }
    
    static void Tester(string name, Func<Task<int>, Task<int>> func)
    {
        ThreadPool.SetMinThreads(200, 200);
        var sw = new System.Diagnostics.Stopwatch();
        sw.Start();
        for (int i = 0; i < 1000000; i++)
        {
            func(DoAsync()).Wait();
        }
        sw.Stop();
        Console.WriteLine("{0}: {1}ms", name, sw.ElapsedMilliseconds);
    }
    

    The output:

    Test1Async: 4207ms
    Test2Async: 4734ms
    

    Now the difference is very marginal, although the async version still performs slightly better. Yet I think such gain is really neglectable, comparable to the actual cost of the asynchronous operation or to the cost of restoring the captured context for when SynchronizationContext.Current != null.

    The bottom line is, if you deal with asynchronous tasks, go for async/await if you have a choice, not for performance reason but for ease of use, readability and maintainability.