6

I have an async asp.net controller. This controller calls an async method. The method that actually performs the async IO work is deep down in my application. The series of methods between the controller and the last method in the chain are all marked with the async modifier. Here is an example of how I have the code setup:

public async Task<ActionResult> Index(int[] ids)
{
    List<int> listOfDataPoints = dataPointService(ids);
    List<Task> dpTaskList = new List<Task>();
    foreach (var x in listOfDataPoints)
    {
        dpTaskList.Add(C_Async(x));
    }

    await Task.WhenAll(dpTaskList);
    return View();
}


private async Task C_Async(int id)
{
    //this method executes very fast
    var idTemp = paddID(id);
    await D_Async(idTemp);
}

private async Task D_Async(string id)
{
    //this method executes very fast
    await E_Async(id);
}

private async Task E_Async(string url)
{
    //this method performs the actual async IO
    result = await new WebClient().DownloadStringTaskAsync(new Uri(url))
    saveContent(result);
}

As you can see the controller calls C_Async(x) asynchronously then there is a chain of async methods to E_Async. There are methods between the controller and E_Async and all have the async modifier. Is there a performance penalty since there are methods using the async modifyer but not doing any async IO work?

Note: This is a simplified version of the real code there are more async methods between the controller and the E_Async method.

i3arnon
  • 113,022
  • 33
  • 324
  • 344
Luke101
  • 63,072
  • 85
  • 231
  • 359
  • possible duplicate of [Async all the way down?](http://stackoverflow.com/questions/12016322/async-all-the-way-down) – Akash Kava Jan 12 '14 at 07:13
  • 1
    Of course there is overhead. But your question leads the wrong way. If the called tasks are fast then you can call them synchronously without async. If you want to be sure: Measure. – Alois Kraus Jan 12 '14 at 07:14
  • @Alois - Exactly, how would i call the methods synchronously while calling the last method asynchronously? I am a beginner at async programming so feel free to reword my post. – Luke101 Jan 12 '14 at 07:19
  • 1
    The structuring is explained by I3arnon. Once you are running inside a task you do not need the async pattern anymore since your are already running asynchronously. For the performance implications you have a look at my answer. If you are not after microseconds but a responsive web page anything will do which as long as you are doing your work no on the main thread. – Alois Kraus Jan 12 '14 at 09:46
  • There's always a performance penalty. When the async work is something like blocking IO, you'll get an availability gain. But if it's just CPU work, you'll only get the penalty with no gains. – Paulo Morgado Jan 13 '14 at 12:44

2 Answers2

5

Yes. There is a penalty (though not a huge one), and if you don't need to be async don't be. This pattern is often called "return await" where you can almost always remove both the async and the await. Simply return the task you already have that represents the asynchronous operations:

private Task C_Async(int id)
{
    // This method executes very fast
    var idTemp = paddID(id);
    return D_Async(idTemp);
}

private Task D_Async(string id)
{
    // This method executes very fast
    return E_Async(id);
}

In this specific case Index will only await the tasks that E_Async returns. That means that after all the I/O is done the next line of code will directly be return View();. C_Async and D_Async already ran and finished in the synchronous call.

i3arnon
  • 113,022
  • 33
  • 324
  • 344
  • 3
    This is a good answer, but there's one more important detail: the `async` modifier will also capture exceptions from the synchronous portion of the method and place them on the returned `Task`, while non-`async` methods will raise exceptions directly. – Stephen Cleary Jan 12 '14 at 12:34
2

You must be careful about the thread message pumps and what async really does. The sample below calls into an async method which calls two other async methods which start two tasks to do the actual work which wait 2 and 3 seconds.

13.00 6520 .ctor Calling async method
13.00 6520 RunSomethingAsync Before
13.00 6520 GetSlowString Before
13.00 5628 OtherTask Sleeping for 2s
15.00 5628 OtherTask Sleeping done
15.00 6520 GetVerySlow Inside
15.00 2176 GetVerySlow Sleeping 3s
18.00 2176 GetVerySlow Sleeping Done
18.00 6520 RunSomethingAsync After GetSlowOtherTaskResultGetVerySlowReturn

As you can see the calls are serialized which might not be what you want when you after performance. Perhaps the two distinct await calls do not depend on each other and can be started directly as tasks.

All methods until GetSlowStringBefore are called on the UI or ASP.NET thread that started the async operation (if it it has a message pump). Only the last call with the result of the operation are marshalled back to the initiating thread.

The performance penalty is somewhere in the ContextSwitch region to wake up an already existing thread. This should be somewhere at microsecond level. The most expensive stuff would be the creation of the managed objects and the garbage collector cleaning up the temporary objects. If you call this in a tight loop you will be GC bound because there is an upper limit how many threads can be created. In that case TPL will buffer your tasks in queues which require memory allocations and then drain the queues with n worker threads from the thread pool.

On my Core I7 I get an overhead of 2microseconds for each call (comment out the Debug.Print line) and a memory consumption of 6,5GB for 5 million calls in a WPF application which gives you a memory overhead of 130KB per asynchronous operation chain. If you are after high scalability you need to watch after your GC. Until Joe Duffy has finished his new language we have to use CLR we currently have.

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        Print("Calling async method");
        RunSomethingAsync();
    }

    private async void RunSomethingAsync()
    {
        Print("Before");
        string msg = await GetSlowString();
        Print("After " + msg);
        cLabel.Content = msg;
    }

    void Print(string message, [CallerMemberName] string method = "")
    {
        Debug.Print("{0:N2} {1} {2} {3}", DateTime.Now.Second, AppDomain.GetCurrentThreadId(), method, message);
    }

    private async Task<string> GetSlowString()
    {
        Print("Before");

        string otherResult = await OtherTask();

        return "GetSlow" + otherResult + await GetVerySlow(); ;
    }

    private Task<string> OtherTask()
    {
        return Task.Run(() =>
        {
            Print("Sleeping for 2s");
            Thread.Sleep(2 * 1000);
            Print("Sleeping done");
            return "OtherTaskResult";
        });
    }

    private Task<string> GetVerySlow()
    {
        Print("Inside");
        return Task.Run(() =>
        {
            Print("Sleeping 3s");
            Thread.Sleep(3000);
            Print("Sleeping Done");
            return "GetVerySlowReturn";
        });
    }
}
Alois Kraus
  • 13,229
  • 1
  • 38
  • 64
  • I too would be interested to know. What you're talking about does make sense. Perhaps, the down-voter didn't like that you refer to thread's message pump and WPF, while the OP's question mentions ASP.NET? – noseratio Jan 12 '14 at 11:11
  • If WPF or ASP.NET is used to show async is irrelevant as long as the thread has a SynchronizationContext set to which the result can be posted back to it. But it certainly can confuse people. – Alois Kraus Jan 12 '14 at 11:21
  • 1
    Right, although I'd say that the performance hit of of `asyc/await` also largely depends on the particular implementation of `SynchronizationContext`. I.e., posting to a UI thread message loop with `WinFormsSynchronizationContext` is probably more costly than just flowing logical call context data when continuing on a different thread with `AspNetSynchronizationContext`. – noseratio Jan 12 '14 at 11:38
  • @AloisKraus a noob question: does tagging the downvoter actually works? – i3arnon Jan 12 '14 at 16:28
  • 1
    @l3arnon: I do not think so. But we are in a free world where one can make his point by commenting or not. – Alois Kraus Jan 12 '14 at 23:08
  • The article by Joe is very interesting too bad the language is geared only for system programming. – Luke101 Jan 13 '14 at 14:28