By Royi Namir


2014-05-26 13:55:19 8 Comments

(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 ?

5 comments

@noseratio 2014-05-26 23:21:46

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.

@StriplingWarrior 2014-05-26 13:59:17

Can such scenario occur?

Absolutely. For this reason, you should be conscientious about where you use async code. Typically you're best using it for methods that will actually perform an asynchronous operation (disk or network I/O, for example). The time that these operations take usually far outweighs the cost of scheduling tasks on threads. Also, at the operating system level, these sorts of operations are inherently asynchronous, so you're actually removing a layer of abstraction by using async methods.

Even in these cases, though, you will likely not see a noticeable performance difference by switching to asynchronous code unless you are able to take advantage of concurrency. For example, the code you posted would probably see no real performance gain unless it were changed to something like this:

await Task.WhenAll(new[]{A(), B(), C(), D(), ...});

@Kirk Woll 2014-05-26 13:59:52

But this presumes async is always leveraging a thread. Which is not so.

@StriplingWarrior 2014-05-26 14:09:43

@KirkWoll: Please clarify. I specifically said that you should use async when you're performing operations that are actually asynchrnous. I can't think of any non-asynchronous operations that could be translated to async methods without leveraging a thread.

@Kirk Woll 2014-05-26 16:36:59

Take a look at Stephen Clearly's article, There is no thread; Erik Meijer once linked to it approvingly. :)

@Stephen Cleary 2014-05-26 14:03:46

Yes, in theory. Not normally, in the real world.

In the common case, async is used for I/O-bound operations, and the overhead of thread management is undetectable in comparison to them. Most of the time, asynchronous operations either take a very long time (compared to thread management) or are already completed (e.g., a cache). Note that async has a "fast path" that kicks in if the operation is already completed, where it does not yield the thread.

For more information, see the Zen of Async and Async Performance.

@jgauffin 2014-05-26 14:09:39

Even if it's performed synchronously it will take longer as the TPL code has to run. Remember, it's as the OP pointed out a theoretical question.

@Stephen Cleary 2014-05-26 14:38:08

@jgauffin: I'm not entirely sure what you mean by "the TPL code has to run". There's no scheduling going on, if that's what you're thinking.

@jgauffin 2014-05-26 14:39:50

What I mean is that if you benchmark code using TPL and code doing the operations directly the latter will be faster as TPL adds some overhead (even if it's tiny).

@svick 2014-05-26 20:26:25

@jgauffin await code that uses the fast path uses only a tiny amount of TPL (the equivalents of Task.FromResult() and Task.Result). Is that what you mean? It certainly doesn't do anything similar to Task.Run(), or anything like that.

@M.kazem Akhgary 2018-11-19 12:41:35

I have thousand small awaits when writing to a file. But there is a 8MB buffer. So the real await is only hit when this buffer is full and has to be written on disk. This greatly improved the performance instead of writing on disk thousand times

@Yuval Itzchakov 2014-05-26 14:03:14

Yes, of course it can happen. With all the overhead of the creating the state machine, yielding back and forth the control and using IOCP threads. But as said, the TPL is pretty optimized. For example, lets not forget that if your TaskAwaitable finishes quickly, there might be no overhead and it will execute synchronously, which might happen often with quick operations.

@TomTom 2014-05-26 13:59:39

Yes, it can happen. Also do not forget that - all efficiency in there that you can program - the task system DOES have overhead.

If you get too granulkar with something like this, the synchronization overhead CAN kill you. THAT SAID: Tasks are quite efficienttly programmed.

But the old rule sticks: do not go super granular. SOmetimes optimization helps.

Related Questions

Sponsored Content

13 Answered Questions

[SOLVED] Is the recommendation to include CSS before JavaScript invalid?

3 Answered Questions

[SOLVED] Do you have to put Task.Run in a method to make it async?

17 Answered Questions

[SOLVED] Using async/await with a forEach loop

5 Answered Questions

3 Answered Questions

[SOLVED] Why is HttpContext.Current null after await?

10 Answered Questions

[SOLVED] Improve INSERT-per-second performance of SQLite?

21 Answered Questions

[SOLVED] How and when to use ‘async’ and ‘await’

9 Answered Questions

[SOLVED] Swift Beta performance: sorting arrays

5 Answered Questions

[SOLVED] Using async/await for multiple tasks

Sponsored Content