Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Not awaiting an async call is still async, right?

I'm sorry if this is a silly question (or a duplicate).

I have a function A:

public async Task<int> A(/* some parameters */)
{
    var result = await SomeOtherFuncAsync(/* some other parameters */);

    return (result);
}

the I have another function B, calling A but not using the return value:

public Task B(/* some parameters */)
{
    var taskA = A(/* parameters */); // #1

    return (taskA);
}

Note that B is not declared async and is not awaiting the call to A. The call to A is not a fire-and-forget call - B is called by C like so:

public async Task C()
{
    await B(/* parameters */);
}

Notice that at #1, there's no await. 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.

I tried pointing out that just because we don't wait for the results inside B, the task chain is awaited and the nature of the code inside A doesn't change just because we don't await it. Since the return value from A is not needed, 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).

My coworker is very insistent and I began to doubt myself. Is my understanding wrong?

like image 947
xxbbcc Avatar asked Sep 20 '19 22:09

xxbbcc


People also ask

What happens when you call an async method without await?

The call to the async method starts an asynchronous task. However, because no Await operator is applied, the program continues without waiting for the task to complete. In most cases, that behavior isn't expected.

Does async work without await?

In this way, an async function without an await expression will run synchronously. If there is an await expression inside the function body, however, the async function will always complete asynchronously. Code after each await expression can be thought of as existing in a .then callback.

How do I stop async calls?

You can cancel an asynchronous operation after a period of time by using the CancellationTokenSource. CancelAfter method if you don't want to wait for the operation to finish.

What is the difference between await and async?

In using async and await, async is prepended when returning a promise, await is prepended when calling a promise. try and catch are also used to get the rejection value of an async function.


Video Answer


2 Answers

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 awaited, 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.

like image 179
Eric Lippert Avatar answered Oct 26 '22 07:10

Eric Lippert


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.

like image 6
Ordoshsen Avatar answered Oct 26 '22 06:10

Ordoshsen