Why use `await AbcAsync()` instead of `Abc()`

I'm going through the tutorial in this page: https://learn.microsoft.com/en-us/aspnet/core/tutorials/first-mvc-app/controller-methods-views?view=aspnetcore-9.0 I don't understand this bit of generated code inside an async controller method:
// . . .
_context.Update(movie);
await _context.SaveChangesAsync();
// . . .
// . . .
_context.Update(movie);
await _context.SaveChangesAsync();
// . . .
why call _context.SaveChangesAsync() and then immediately await it, instead of the synchronous version _context.SaveChanges() (which does exist)? I assume that this is because the async method will only temporarily free the thread to do other work once it hits an await, but that doesn't make sense, because that's the whole point of an async method (whether it uses await inside its body or not).
Part 6, controller methods and views in ASP.NET Core
Part 6, add a model to an ASP.NET Core MVC app
26 Replies
Jimmacle
Jimmacle5d ago
yes, using the async method allows the thread to do other work while waiting for something to happen outside of the CPU in this case, network I/O to your database
aetherclouds
aethercloudsOP5d ago
but the controller method wrapping that piece of code is already async - wouldn't that be enough?
Jimmacle
Jimmacle5d ago
no, an async method that doesn't await anything is not actually async
aetherclouds
aethercloudsOP5d ago
oh huh
Jimmacle
Jimmacle5d ago
ultimately what makes it async is some code deep down in the call stack that starts a non-CPU-bound operation, which you await all the way back up the call stack
aetherclouds
aethercloudsOP5d ago
so unlike in javascript, where if you pass an async callback to something that expects a synchronous one, and where the default behavior is to not "wait" for the async function (so it will run asynchronously), c# "awaits" by default?
Jimmacle
Jimmacle5d ago
not really by default, you can start an async operation and store the returned Task in a variable to await the result of later
aetherclouds
aethercloudsOP5d ago
so if the call on an async method doesn't block and returns an unfinished task, doesn't that mean it's running asynchronously?
jcotton42
jcotton425d ago
The only thing the async keyword does is enable the use of await in the method body.
Jimmacle
Jimmacle5d ago
async methods run synchronously until the first await
jcotton42
jcotton425d ago
(Well, and it implicitly wraps the return value in a task)
Jimmacle
Jimmacle5d ago
maybe i should make it a point that this isn't about multithreading this is about throughput async/await allows you to avoid blocking threads when you don't actually have any work for them to do, (overwhelmingly, when doing IO)
aetherclouds
aethercloudsOP5d ago
so there's no concurrency going on, or at least there is but it's predictable and not done at a CPU level?
Jimmacle
Jimmacle5d ago
$nothread
MODiX
MODiX5d ago
There Is No Thread
This is an essential truth of async in its purest form: There is no thread.
Jimmacle
Jimmacle5d ago
it's useful in situations like web servers, where instead of taking a thread to handle a full request from start to end the actual sending/receiving of data can be done asynchronously and the same thread can juggle multiple requests when there is actually CPU work for the thread to do for them which means your threads are doing a lot less waiting around for IO and your web server can handle more requests at once also, notably in this case you cannot perform concurrent database operations with the same DbContext instance
Petris
Petris5d ago
well the continuations can run on whatever thread they want
Console.WriteLine(Environment.ManagedThreadId); // 1
await Task.Yield().ConfigureAwait(false);
Console.WriteLine(Environment.ManagedThreadId); // 5
Console.WriteLine(Environment.ManagedThreadId); // 1
await Task.Yield().ConfigureAwait(false);
Console.WriteLine(Environment.ManagedThreadId); // 5
uh what, bot seems messed up
MODiX
MODiX5d ago
Petris
REPL Result: Success
Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
await Task.Delay(15).ConfigureAwait(false);
Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
await Task.Delay(15).ConfigureAwait(false);
Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
Console Output
12
12
12
12
Compile: 317.869ms | Execution: 98.090ms | React with ❌ to remove this embed.
Petris
Petris5d ago
well not in this case apparently
MODiX
MODiX5d ago
jcotton42
REPL Result: Success
SynchronizationContext.Current
SynchronizationContext.Current
Compile: 212.658ms | Execution: 16.611ms | React with ❌ to remove this embed.
aetherclouds
aethercloudsOP4d ago
I'm still on this, just wanted to add some more stuff that might help me later it turns out that await doesn't necessarily yield to the calling function, but rather to the called function, until it hits non cpu-bound work.
static Task C()
{
Console.WriteLine(2);
double result = 0;
for (long i = 0; i <= 100000000; i++)
{
result += Math.Sqrt(i) * Math.Sin(i) / Math.Cos(i);
if (i == 100000000)
{
Console.WriteLine("checkpoint");
}
}
Console.WriteLine(3);
return Task.CompletedTask;
}

static async Task B()
{
await C();
Console.WriteLine(4);
return;
}

public static void Main(string[] args)
{
Console.WriteLine(1);
var x = B();
Console.WriteLine(5);
Task.WaitAll(x);
}
static Task C()
{
Console.WriteLine(2);
double result = 0;
for (long i = 0; i <= 100000000; i++)
{
result += Math.Sqrt(i) * Math.Sin(i) / Math.Cos(i);
if (i == 100000000)
{
Console.WriteLine("checkpoint");
}
}
Console.WriteLine(3);
return Task.CompletedTask;
}

static async Task B()
{
await C();
Console.WriteLine(4);
return;
}

public static void Main(string[] args)
{
Console.WriteLine(1);
var x = B();
Console.WriteLine(5);
Task.WaitAll(x);
}
in contrast:
static async Task C()
{
Console.WriteLine(2);
await Task.Delay(2000);
Console.WriteLine(4);
return;
}

static async Task B()
{
await C();
Console.WriteLine(5);
return;
}

public static void Main(string[] args)
{
Console.WriteLine(1);
var x = B();
Console.WriteLine(3);
Task.WaitAll(x);
}
static async Task C()
{
Console.WriteLine(2);
await Task.Delay(2000);
Console.WriteLine(4);
return;
}

static async Task B()
{
await C();
Console.WriteLine(5);
return;
}

public static void Main(string[] args)
{
Console.WriteLine(1);
var x = B();
Console.WriteLine(3);
Task.WaitAll(x);
}
I guess what really sticks out to me is 1. actually concurrent work depends completely on the called function. so I guess if you somehow "messed up" by not buying into async/await, your chain of callers will just be waiting for you synchronously 1. the whole point of async is efficiently doing I/O bound work, rather than "doing concurrent work" in general, and because of point 1, you have to hope that your call stack will hit an io bound function at some point, and an example of badly done async would be:
static Task C()
{
Console.WriteLine(2);
var t = Task.Delay(3000);
Task.WaitAll(t);
Console.WriteLine(3);
return Task.CompletedTask;
}

static async Task B()
{
await C();
Console.WriteLine(4);
return;
}

public static void Main(string[] args)
{
Console.WriteLine(1);
var x = B();
Console.WriteLine(5);
Task.WaitAll(x);
}
static Task C()
{
Console.WriteLine(2);
var t = Task.Delay(3000);
Task.WaitAll(t);
Console.WriteLine(3);
return Task.CompletedTask;
}

static async Task B()
{
await C();
Console.WriteLine(4);
return;
}

public static void Main(string[] args)
{
Console.WriteLine(1);
var x = B();
Console.WriteLine(5);
Task.WaitAll(x);
}
now I'm just wondering if, suppose Main is busy but C returns, then B will continue running until completion (or until it hits another await), THEN back to Main (the answer is: this is when concurrency kicks in and they run effectively at the same time) thanks for linking this article! would this be a good basic model for a web server?
for (;;) {
// returns new socket with remote address and port
var conn = await ListenAsync(address, port);
// HandleConnection will immediately yield, so we'll always be listening to the socket AND handling one or multiple connections
await HandleConnection(conn);
}

void HandleConnection(Socket conn) {
Task.Yield();
// do useful work
// . . .
}
for (;;) {
// returns new socket with remote address and port
var conn = await ListenAsync(address, port);
// HandleConnection will immediately yield, so we'll always be listening to the socket AND handling one or multiple connections
await HandleConnection(conn);
}

void HandleConnection(Socket conn) {
Task.Yield();
// do useful work
// . . .
}
Jimmacle
Jimmacle4d ago
not quite, if you await it in the loop your server will only process one connection at a time this is actually a time where you wouldn't want to await it and just let it float away on the thread pool (and add some mechanism to observe any exceptions that were thrown)
aetherclouds
aethercloudsOP4d ago
actually I realized I just forgot to include yield in my mental model lol, like will it be doing things concurrently now, since there's nothing to wait for (otherwise handleConnection wouldn't know when to continue)?
Jimmacle
Jimmacle4d ago
if HandleConnection is async and immediately awaits something, if you don't await HandleConnection itself then your loop and HandleConnection will effectively run at the same time
aetherclouds
aethercloudsOP4d ago
oh right I forgot you have to await Task.Yield(), I thought Yield was magically doing that on its own
Jimmacle
Jimmacle4d ago
since the loop isn't waiting for the result of the method it will just go straight back to listening, and when the method has work to do some thread on the thread pool will pick it up note that if you never observe the Task representing the async operation (by awaiting, etc.) then you will never see if an exception is thrown by the method

Did you find this page helpful?