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
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.
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";
});
}
}