Not awaiting an async call is still async, right?
You are right. Creating a task does only that and it does not care when and who will await its result. Try putting await Task.Delay(veryBigNumber);
in SomeOtherFuncAsync
and the console output should be what you would expect.
This is called eliding and I suggest you read this blogpost, where you can see why you should or should not do such thing.
Also some minimal (little convoluted) example copying your code proving you right:
class Program
{
static async Task Main(string[] args)
{
Console.WriteLine($"Start of main {Thread.CurrentThread.ManagedThreadId}");
var task = First();
Console.WriteLine($"Middle of main {Thread.CurrentThread.ManagedThreadId}");
await task;
Console.WriteLine($"End of main {Thread.CurrentThread.ManagedThreadId}");
}
static Task First()
{
return SecondAsync();
}
static async Task SecondAsync()
{
await ThirdAsync();
}
static async Task ThirdAsync()
{
Console.WriteLine($"Start of third {Thread.CurrentThread.ManagedThreadId}");
await Task.Delay(1000);
Console.WriteLine($"End of third {Thread.CurrentThread.ManagedThreadId}");
}
}
This writes Middle of main
before End of third
, proving that it is in fact asynchronous. Furhtermore you can (most likely) see that the ends of functions run on different thread than the rest of the program. Both beginnings and the middle of main will always run on the same thread because those are in fact synchrnous (main starts, calls the function chain, third returns (it may return at the line with the await
keyword) and then main continues as if there was no asynchronous function ever involved. The endings after the await
keywords in both functions may run on any thread in the ThreadPool (or in synchronization context you are using).
Now it is interesting to note, that if Task.Delay
in Third
did not take very long and actually finished synchronously, all of this would run on a single thread. What's more, even though it would run asynchronously, it might all run on a single thread. There is no rule stating that an async function will use more than one thread, it may very well just do some other work while waiting for some I/O task to finish.
I'm sorry if this is a silly question
It is not a silly question. It is an important question.
I have a coworker that claims that this makes the call to A synchronous and he keeps coming up with Console.WriteLine logs that seemingly prove his point.
That is the fundamental problem right there, and you need to educate your coworker so that they stop misleading themselves and others. There is no such thing as an asynchronous call. The call is not the thing that is asynchronous, ever. Say it with me. Calls are not asynchronous in C#. In C#, when you call a function, that function is called immediately after all the arguments are computed.
If your coworker or you believes that there is such a thing as an asynchronous call, you are in for a world of pain because your beliefs about how asynchrony works will be very disconnected from reality.
So, is your coworker correct? Of course they are. The call to A
is synchronous because all function calls are synchronous. But the fact that they believe that there is such a thing as an "asynchronous call" means that they are badly mistaken about how asynchrony works in C#.
If specifically your coworker believes that await M()
somehow makes the call to M()
"asynchronous", then your coworker has a big misunderstanding. await
is an operator. It's a complicated operator, to be sure, but it is an operator, and it operates on values. await M()
and var t = M(); await t;
are the same thing. The await happens after the call because the await
operates on the value that is returned. await
is NOT an instruction to the compiler to "generate an asynchronous call to M()" or any such thing; there is no such thing as an "asynchronous call".
If that is the nature of their false belief, then you've got an opportunity to educate your coworker as to what await
means. await
means something simple but powerful. It means:
- Look at the
Task
that I'm operating on. - If the task is completed exceptionally, throw that exception
- If the task is completed normally, extract that value and use it
- If the task is incomplete, sign up the remainder of this method as the continuation of the awaited task, and return a new
Task
representing this call's incomplete asynchronous workflow to my caller.
That's all that await
does. It just examines the contents of a task, and if the task is incomplete, it says "well, we can't make any progress on this workflow until that task is complete, so return to my caller who will find something else for this CPU to do".
the nature of the code inside A doesn't change just because we don't await it.
That's correct. We synchronously call A
, and it returns a Task
. The code after the call site does not run until A
returns. The interesting thing about A
is that A
is allowed to return an incomplete Task
to its caller, and that task represents a node in an asynchronous workflow. The workflow is already asynchronous, and as you note, it makes no difference to A
what you do with its return value after it returns; A
has no idea whether you are going to await
the returned Task
or not. A
just runs as long as it can, and then either it returns a completed-normally task, or a completed-exceptionally task, or it returns an incomplete task. But nothing you do at the call site changes that.
Since the return value from A is not needed, there's no need to await the task at the call site
Correct.
there's no need to await the task at the call site, as long as someone up the chain awaits it (which happens in C).
Now you've lost me. Why does anyone have to await the Task
returned by A
? Say why you believe that someone is required to await
that Task
, because you might have a false belief.
My coworker is very insistent and I began to doubt myself. Is my understanding wrong?
Your coworker is almost certainly wrong. Your analysis seems correct right up to the bit where you say that there is a requirement that every Task
be await
ed, which is not true. It is strange to not await
a Task
because it means that you wrote a program where you started an operation and do not care about when or how it completes, and it certainly smells bad to write a program like that, but there is not a requirement to await
every Task
. If you believe that there is, again, say what that belief is and we'll sort it out.