0

If I have the following code:

public async Task<SomeObject> ActionMethodAsync()
{
   return await someAsyncMethod();
}

Should I change it to:

public SomeObject ActionMethod()
{
   return someAsyncMethod().GetAwaiter().GetResult();
}

I'm not sure once control returns to the caller of ActionMethodAsync, what exactly will happen and if anything beneficial is achieved.

Wouldn't it yield a similar result to the following compiled code by the compiler, or it's a threadpool thread given by the IIS, and it won't be blocked if we go down the await route? I feel like we will still be using a single thread, regardless of which option we chose to go down on using. (Without the extra cost of switching thread contexts and the async state machine.)

SpiritBob
  • 2,355
  • 3
  • 24
  • 62
  • The first does not block, the second does. They're completely different, and you don't want to synchronously wait on an `async` method if you can at all help it. What you *can* change it to without a change of semantics is `return someAsyncMethod()` (without `async`, but keeping the return type the same). – Jeroen Mostert Nov 04 '19 at 09:40
  • @JeroenMostert won't that return an incomplete task to the user hitting the action? – SpiritBob Nov 04 '19 at 09:41
  • A Task represents a possibly ongoing operation, if your workflow cannot proceed past that point without the result of that operation then you should await that task there. – Andrea Scarcella Nov 04 '19 at 09:43
  • 1
    @SpiritBob: of course -- and that's the point. The ASP.NET framework is the one ultimately awaiting your tasks (or rather scheduling continuations, which is much more efficient), not some end user. – Jeroen Mostert Nov 04 '19 at 11:30
  • @JeroenMostert Wow, you're right! It actually completes the task both in ASP.NET and ASP.NET Core. Does that mean that I don't need to use await/async on the controller? Simply returning the Task is enough? – SpiritBob Nov 04 '19 at 12:15
  • 1
    Sure, though it's actually intended to allow full `async` / `await` in your controller. The case where you directly return the result of another `async` method is a bit unusual. For consistency's sake, most people will still write the method as `async` and then do `return await`, in case you need to make things more complicated later, even though this accomplishes the same thing as returning the result directly and it has a tad more overhead (because the compiler still generates a whole async state machine in the background). – Jeroen Mostert Nov 04 '19 at 12:21
  • Thank yo so much @JeroenMostert! I wasn't aware of this at all. I'm currently reading the [following](https://learn.microsoft.com/en-us/aspnet/mvc/overview/performance/using-asynchronous-methods-in-aspnet-mvc-4) thread which might shine some light upon from where all this is coming from. It's quite out-dated, but if you know a better source, please post it here for the others to see! – SpiritBob Nov 04 '19 at 12:30
  • 1
    ASP.NET Core in particular is *fully* asynchronous -- the most recent versions actually [contain code that will produce errors](https://github.com/aspnet/AspNetCore/issues/7644) when they detect synchronous I/O, as this has been found to lead to thread pool starvation. That's how badly they want you to go `async` all the way. ASP.NET "merely" added `async` support, but even that was 7 years ago now (look at the date on the article). Having all request code run asynchronously (decoupling it from threads) has great benefits for scalability. – Jeroen Mostert Nov 04 '19 at 12:35

3 Answers3

3

That answer you linked to is talking about a console app, so that's a different story.

Microsoft has some really well-written articles about Asynchronous programming with async and await that are well-worth reading. The example it uses of making breakfast is really good in helping to understand what asynchronous programming is all about.

The key thing to remember is that there is a cap on the number of threads that ASP.NET can use. It's configurable, but there will always still be a maximum. So the more you can do with one thread, the better.

Let's use some real numbers as an example. The default thread count per CPU is 20. So let's say you're on a quad-core processor. The max thread count will be 80 (it might actually be 160 if it counts hyper-threading as an extra CPU, but we'll say 80).

Now let's say 80 HTTP requests come in at the same time, all of which require making I/O requests (like, to a database). They all get processed at the same time and start their I/O request. Now, while they're waiting for a response, another request comes in. What happens with it will depend on whether your code is asynchronous or not.

  • If your code is synchronous, all 80 threads are locked, waiting for a response. So that 81st request will not get served until one of those 80 requests finish.
  • If your code is asynchronous, all 80 threads will be free and the 81st request will be served right away. As each I/O request finished, it's put on one of those 80 threads to finish up and return a response to the caller.

Asynchronous programming is not about returning a single request faster (in fact, it will be a tiny bit slower). It's about the overall performance of your application.

Now, on to the example you have. There is one option that you didn't consider, and that is just returning the Task without awaiting it:

public Task<SomeObject> ActionMethodAsync()
{
   return someAsyncMethod();
}

That will be marginally (probably not actually noticeably) faster. There is an old question about that with some good answers, but in short:

  • If you're only calling one asynchronous method, and you will be returning the result of it, then just return the Task directly.
  • If you need to call multiple asynchronous methods, or you need to do some work with the result from an asynchronous call, then you'll have to use async and await.
Gabriel Luci
  • 38,328
  • 4
  • 55
  • 84
  • I'm guessing if you wrapped the returned task in a try/catch block, you'll need to await it, because the exception won't be captured - you'll be returning the task directly, right? ExceptionFilters *should* work though. – SpiritBob Nov 04 '19 at 16:01
  • 1
    Right, exactly. And whether you want to do that or not depends on where you want to handle the exception. It might make sense to handle it there, or just let the calling method handle it. – Gabriel Luci Nov 04 '19 at 16:05
1

Using async/await will allow IIS threads to park requests awaiting on tasks (typically asynchronous i/o requests) temporarily, do other work (serve new incoming requests, resume other requests previously waiting on tasks, for instance), and resume them when the corresponding tasks are completed thereby allowing higher throughput.

Source: https://learn.microsoft.com/en-us/dotnet/standard/async-in-depth#what-does-this-mean-for-a-server-scenario

Andrea Scarcella
  • 3,233
  • 2
  • 22
  • 26
0

There is more to it.

Here are some reasons why yous should stick with async/await

1. You're not meant to use GetResult

TaskAwaiter.GetResult Method states

This API supports the product infrastructure and is not intended to be used directly from your code.

2. Without await your code is not longer asynchronous

By dropping await you're getting rid of the async magic and your code becomes synchronous and blocking and each request uses a thread until it's finished.


Lot's of work, not many threads.

I hope this little example will be able to showcase how easy it is for a framework to deal with more requests then there are threads when Tasks are used.

Think of it as 3 waiters being able to serve 10 tables.

static async Task DoAsync(int index) 
{
    var threadId = Thread.CurrentThread.ManagedThreadId;
    await Task.Delay(TimeSpan.FromMilliseconds(index*100));
    Console.WriteLine($"task: {index}, threads: {threadId} -> {Thread.CurrentThread.ManagedThreadId}");
}

public static async Task Main()
{
    Console.WriteLine($"thread: {Thread.CurrentThread.ManagedThreadId}: Main");

    var tasks = Enumerable.Range(0, 10).Select( i => DoAsync(i));
    await Task.WhenAll(tasks);

    Console.WriteLine($"thread: {Thread.CurrentThread.ManagedThreadId}: Main Done");
}

Output

thread: 1: Main
task: 0, threads: 1 -> 1
task: 1, threads: 1 -> 4
task: 2, threads: 1 -> 4
task: 3, threads: 1 -> 4
task: 4, threads: 1 -> 4
task: 5, threads: 1 -> 4
task: 6, threads: 1 -> 5
task: 7, threads: 1 -> 5
task: 8, threads: 1 -> 5
task: 9, threads: 1 -> 5
thread: 5: Main Done

As you found yourself, the point of n threads being able to handle x>n request is explicitly expressed in Async in depth's 'What does this mean for a server scenario?' section.

This model works well with a typical server scenario workload. Because there are no threads dedicated to blocking on unfinished tasks, the server threadpool can service a much higher volume of web requests. (...)

State machines

Asynchronous programming is a good source of information about async/await. There are two passages especially relevant to the simplified view of await == GetAwaiter().GetResult().

What happens under the covers

There's a lot of moving pieces where asynchronous operations are concerned. If you're curious about what's happening underneath the covers of Task and Task, checkout the Async in-depth article for more information.

On the C# side of things, the compiler transforms your code into a state machine which keeps track of things like yielding execution when an await is reached and resuming execution when a background job has finished.

tymtam
  • 31,798
  • 8
  • 86
  • 126
  • Simply having await in your code, does not make it asynchronous. If the Async method has no suspensions in it, or those suspensions are able to complete synchronously (there are no Tasks started/work scheduled), you'd finish synchronously, and only hinder yourself from all the added "magic". – SpiritBob Nov 04 '19 at 09:44
  • I still don't see how using await in this context helps us in any way? A single thread will be used regardless. When the calling method suspends and returns back control to `ActionMethodAsync`, `ActionMethodAsync`'s `await` will yield back control to the caller of that method (whatever that is). Isn't that thread there, then just be left waiting? – SpiritBob Nov 04 '19 at 09:48
  • What if you have multiple requests at the same time? – tymtam Nov 04 '19 at 09:50
  • What about it? A single request is always represented by a new thread given by the IIS? it doesn't require you to have an await/async method in order to handle concurrent requests. – SpiritBob Nov 04 '19 at 09:51
  • 1
    @SpiritBob using async/await will allow IIS threads to park requests awaiting on tasks temporarily, do other work (serve new incoming requests, resume other requests previously waiting on tasks), and resume them when the corresponding tasks are completed thereby allowing higher throughput. – Andrea Scarcella Nov 04 '19 at 09:59
  • @AndreaScarcella Nice! Meaning when control returns from `ActionMethodAsync`, the thread will go serve other requests, and at a later point some other thread will resume the work? Could you share the material you got this off? – SpiritBob Nov 04 '19 at 10:02
  • 1
    @SpiritBob: Thanks I found it here, https://learn.microsoft.com/en-us/dotnet/standard/async-in-depth#what-does-this-mean-for-a-server-scenario – Andrea Scarcella Nov 04 '19 at 10:07