8

Why do we need more than one await statement in a C# method?

E.g. here we have three await statements:

using System;
using System.Threading.Tasks;
using Volo.Abp.Data;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Domain.Repositories;

namespace Acme.BookStore
{
    public class BookStoreDataSeederContributor
        : IDataSeedContributor, ITransientDependency
    {
        private readonly IRepository<Book, Guid> _bookRepository;

        public BookStoreDataSeederContributor(IRepository<Book, Guid> bookRepository)
        {
            _bookRepository = bookRepository;
        }

        public async Task SeedAsync(DataSeedContext context)
        {
            if (await _bookRepository.GetCountAsync() > 0)
            {
                return;
            }

            await _bookRepository.InsertAsync(
                new Book
                {
                    Name = "1984",
                    Type = BookType.Dystopia,
                    PublishDate = new DateTime(1949, 6, 8),
                    Price = 19.84f
                }
            );

            await _bookRepository.InsertAsync(
                new Book
                {
                    Name = "The Hitchhiker's Guide to the Galaxy",
                    Type = BookType.ScienceFiction,
                    PublishDate = new DateTime(1995, 9, 27),
                    Price = 42.0f
                }
            );
        }
    }
}

In case we will remove the second and the third await statements in the SeedAsync no extra threads will be blocked, since already after the first await we are not blocking any useful threads and we already allocated an extra thread for the first await. So, by using the second and the third await statements we are allocating the extra two threads.

Am I missing something here? Since abp.io seems to be a big project I suspect that the examples would not be unreasonable and hence there is must be a reason to the use of the three await statements instead of the one.

NineBerry
  • 26,306
  • 3
  • 62
  • 93
hellouworld
  • 525
  • 6
  • 15
  • Do you see any compiler warnings when you remove an await? See where that leads you. – StuartLC Nov 28 '19 at 07:23
  • So, await is not a thread. It create one thread when it need. If system can create one more thread, it creates. Threre is a ThreadPool that see, if new await is apears,and threre is is a free thread that not working, that this thread is used. Otherwise it creates a new thread. In your example, it creates 2\3 thread, due to it's too small number to optimise it. – TemaTre Nov 28 '19 at 07:26
  • Sometimes it can not create thread due to optimisation, For example if call stack has less then 2 awaits (not 2 awaits in method). – TemaTre Nov 28 '19 at 07:39

7 Answers7

4

Why do we need more than one await statement in a C# method?

You need as much await in your code, you want to (a)wait for the execution of the called async method completes. When you call an asynchronous method, it will (at some point!) return a task (incomplete or completed one), what is technically a promise from that method that at some point it will completes its job.

For example _bookRepository.InsertAsync(...) promise that it will insert the item into the repository and it will notify you via the returned Task when it is happened. It is now up to you, the caller, whether you want to wait for it using await, or you do not care if and when this job finished (fire and forget) so you do not use await, and continue to execute the rest of the caller code.

So it is totally valid to remove the await keywords almost everywhere but there is a very high chance it will alter the program flow and could lead to side effects (explanation in the next section).

In case we will remove the second and the third await statements in the SeedAsync no extra threads will be blocked, since already after the first await we are not blocking any useful threads and we already allocated an extra thread for the first await. So, by using the second and the third await statements we are allocating the extra two threads.

There are several misunderstanding here:

  • Calling an async method does not make the code asynchronous. The called code will run synchronously up until the called method, or any child method calls returns a Task, what is not already completed. Awaiting on completed task makes the call and the continuation completely synchronous!
  • Following up on the previous point, async method calls does not create or allocate thread per se. It is up to the called code to "side-load" its job one way or another.
  • Using await keyword will "put" the remaining code after the keyword into a continuation what will run asynchronously, but it say nothing about threads or necessarily creates one! It has to be imagined that the continuation is put into a queue and will be executed at some point by a thread.

there is must be a reason to the use of the three await statements instead of the one.

Without any further investigation into the project you quoted, most probably _bookRepository.InsertAsync(...) methods are not "parallel safe", otherwise await Task.WhenAll(insert1, insert2) format could have been used. Also not using await for the insertions potentially lead to side effect, for example multi threading like race conditions (read state before write has been finished).

EDIT: You can find lots of useful reading material on learn.microsoft.com, such this: https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/async/task-asynchronous-programming-model

I suggest to read them multiple times and make test apps because the topic is more complex than it looks like and filled with small details easy to misinterpret.

3

await will wait until the operation is not executed. So you has 2 async operations, that's why you need to use await.

One await for each async operation (method).

So, you have 3 async methods. You can call it without await, but it will crash. When you call it without await, it will start to execute in another thread, and thread where SeedAsync is executing, will not wait until InsertAsync is executed. It will start second InsertAsync at the same time

So, in your case, you can insert values without await. It will work. But in common case it's better to use await. Because often order of operations is important. await is allow to control the order of operations

Sometimes you need to run some tasks and then wait for all. Then you can use Task.WhenAll(t1,t2,t3)

Daria Pydorenko
  • 1,754
  • 2
  • 18
  • 45
TemaTre
  • 1,422
  • 2
  • 12
  • 20
  • Please, write reason of downvote? In first sentence, user asks "Why do we need more than one await statement in a C# method?" I write answer for it – TemaTre Nov 28 '19 at 07:36
  • I was not the one who downvoted your answer. But your answer is not helpful. You just explain the `await` keyword, but not why to use it three times in a method. – hellouworld Nov 28 '19 at 07:50
  • So, you has 3 async methods.You can call it without await, but it will crash. When you call it without await, it will start to execute in another thread, and thread where SeedAsync is execitung, will not wait until InsertAsync is executed. It will start second InsertAsync at the same time. – TemaTre Nov 28 '19 at 08:02
  • 2
    Why will there be a crush? – hellouworld Nov 28 '19 at 08:04
  • So, in your case, you can insert values without await. It will work. But in common case it's better to use await. Because often order of operations is important. await is allow to control order of operations – TemaTre Nov 28 '19 at 08:07
  • That is reasonable. Thank you. – hellouworld Nov 28 '19 at 08:14
  • You can add your last comment as a part of an answer and then I will accept your answer. Thank you. – hellouworld Nov 28 '19 at 08:15
2

If you remove the last two await, your code will become like this:

public async Task SeedAsync(DataSeedContext context)
{
    if (await _bookRepository.GetCountAsync() == 0)
    {
        _bookRepository.InsertAsync(new Book("Title1"));
        _bookRepository.InsertAsync(new Book("Title2"));
    }
}

Immediately you'll get two warnings:

Warning CS4014
Because this call is not awaited, execution of the current method continues before the call is completed. Consider applying the 'await' operator to the result of the call.

The message of the warning is descriptive enough. Without these two await:

  1. The two InsertAsync tasks will run concurrently. This could cause state corruption in case the InsertAsync manipulates shared state without synchronization.
  2. The callers of the SeedAsync method will receive a Task that will signal its completion before it is actually completed.
  3. Any unhandled exceptions that may occur during the execution of the two InsertAsync tasks will remain unobserved.
Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
1

Yes you do.

When you await, your method is going into suspend mode. So during your first await, this block of code will stop running and will resume when it is appropriate (after the async code has been executed).

Microsoft docs on await

The await operator suspends evaluation of the enclosing async method until the asynchronous operation represented by its operand completes. When the asynchronous operation completes, the await operator returns the result of the operation, if any.

If you remove the rest of the awaits, the code will not stop at their execution. It will just stop at the first one and fire and forget the other two. You should also have compiler warnings if you remove.

Check the explanation here. Without the await, the tasks will be run synchronously. What does that mean. Well long story short, when you wait the thread is open to doing other stuff. When you don't the thread can't be used for other actions outside the scope of the currently running code.

It's a fire and forget situation.

You can find an excellent read here on IO Threads to understand exactly how asynchronous programming works in C#

Athanasios Kataras
  • 25,191
  • 4
  • 32
  • 61
  • 2
    Await does not put the current thread in a suspended state. It just suspends the code logic, but not the thread. – NineBerry Nov 28 '19 at 07:32
  • Sorry, I meant method. Revised the answer. – Athanasios Kataras Nov 28 '19 at 07:51
  • 2
    It's wrong to say that "When you don't the thread can't be used.". The thread will continue to execute normally. The problem is rather that the following code executes before the previously awaited task is finished. – NineBerry Nov 28 '19 at 07:54
  • I thought it was evident by the synchronicity. I expanded the sentence to be more appropriate. Thanks for the comments! They really made me think about how I write and what I mean in the future! – Athanasios Kataras Nov 28 '19 at 08:00
  • *Without the await, the tasks will be run synchronously.* Do you mean concurrently? – Theodor Zoulias Nov 28 '19 at 10:38
  • The original code will be executed synchronously, meaning firing up both async methods without waiting, which may or may not run concurrently. – Athanasios Kataras Nov 28 '19 at 12:25
  • 1
    In my book **synchronous** code means **blocking** code. So since the two tasks are running without blocking the current thread, they can't be synchronous. The definition of [synchronous](https://stackoverflow.com/questions/748175/asynchronous-vs-synchronous-execution-what-does-it-really-mean) has various flavors though. – Theodor Zoulias Nov 28 '19 at 15:01
1

It is necessary to use await operator in all awaitable methods to gain advantage of asynchronous code.

In addition, await does not create new thread.

If you delete await operators, then your code will be still asynchronous, but these methods will be called sequentially, but you will not know when these methods will be finished to execute.

await _bookRepository.InsertAsync(
            new Book
            {
                Name = "1984",
                Type = BookType.Dystopia,
                PublishDate = new DateTime(1949, 6, 8),
                Price = 19.84f
            }
        );

await _bookRepository.InsertAsync(
            new Book
            {
                Name = "The Hitchhiker's Guide to the Galaxy",
                Type = BookType.ScienceFiction,
                PublishDate = new DateTime(1995, 9, 27),
                Price = 42.0f
            }
        );

If _bookRepository.InsertAsync has the code that looks like this:

public async Task Add<T>(string url)
{
    using (var context = new BloggingContext())
    {
        var blog = new Blog { Url = url };
        context.Blogs.Add(blog);
        await context.SaveChangesAsync();
    }
}

then the above code will be executed on the current thread, but current thread will not be blocked until insertion in database is completed. So, e.g. user interface in desktop applications will not be frozen.

If your implementation of InsertAsync looks like this:

public async Task Add<T>(string url)
{
    using (var context = new BloggingContext())
    {
        var blog = new Blog { Url = url };
        await Task.Run( () =>
        {
            context.Blogs.Add(blog);
        }); 
    }
}

Then your code can be executed on other threads. CLR decides what thread will be used to perform Task.

As Microsoft docs says:

An example of synchronous code:

static void Main(string[] args)
{
    Coffee cup = PourCoffee();
    Console.WriteLine("coffee is ready");
    Egg eggs = FryEggs(2);
    Console.WriteLine("eggs are ready");
    Bacon bacon = FryBacon(3);
    Console.WriteLine("bacon is ready");
    Toast toast = ToastBread(2);
    ApplyButter(toast);
    ApplyJam(toast);
    Console.WriteLine("toast is ready");
    Juice oj = PourOJ();
    Console.WriteLine("oj is ready");

    Console.WriteLine("Breakfast is ready!");
}

The computer will block on each statement until the work is complete before moving on to the next statement. That creates an unsatisfying breakfast. The later tasks wouldn't be started until the earlier tasks had completed. It would take much longer to create the breakfast, and some items would have gotten cold before being served.

If you want the computer to execute the above instructions asynchronously, you must write asynchronous code.

Don't block, await instead

static async Task Main(string[] args)
{
    Coffee cup = PourCoffee();
    Console.WriteLine("coffee is ready");
    Egg eggs = await FryEggs(2);
    Console.WriteLine("eggs are ready");
    Bacon bacon = await FryBacon(3);
    Console.WriteLine("bacon is ready");
    Toast toast = await ToastBread(2);
    ApplyButter(toast);
    ApplyJam(toast);
    Console.WriteLine("toast is ready");
    Juice oj = PourOJ();
    Console.WriteLine("oj is ready");

    Console.WriteLine("Breakfast is ready!");
}

This code doesn't block while the eggs or the bacon are cooking. This code won't start any other tasks though. You'd still put the toast in the toaster and stare at it until it pops. But at least, you'd respond to anyone that wanted your attention. In a restaurant where multiple orders are placed, the cook could start another breakfast while the first is cooking.

UPDATE:

When await operator is deleted, then we see the following warning in Visual Studio:

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

You can try on your own:

static async void BarAsync()
{
    Console.WriteLine("The start of FooMethod"); // synchronous
    await Task.Run(() => FooMethod());                //  asynchronous
    Console.WriteLine("The end of FooMethod");
}

static void FooMethod()
{   
    Thread.Sleep(8000);
    Console.WriteLine("Hello World");
}
StepUp
  • 36,391
  • 15
  • 88
  • 148
  • 1
    Comments are not for extended discussion; this conversation has been [moved to chat](https://chat.stackoverflow.com/rooms/203353/discussion-on-answer-by-stepup-why-do-we-need-more-than-one-await-statement-in). – Samuel Liew Nov 29 '19 at 20:27
0

I'm assuming that the other two functions that you are using are all asynchronous

the "InsertAsync" and "GetCountAsync"

Remember, when calling asynchronous functions you always have to await them. It means you have to wait for that function before next step can proceed running. Read more about async and await.

It is not required to have multiple await in a asynchronous function, but it is required to await a asynchronous function that you are calling inside of your function.

Ivan Apungan
  • 534
  • 6
  • 17
  • Why is it required to await an asynchronous function? – hellouworld Nov 28 '19 at 07:45
  • It is bad practice if you do not await them, for example you have a UI, that is calling multiple asynchronous api, if you do not await those calls then the UI might render while the response from the api haven't returned yet. It might destroy the UI you are rendering if it is dependent to the data you are waiting from the API. If you think this solved your questions, please upvote my answer. thanks. – Ivan Apungan Nov 28 '19 at 07:47
  • Here it is not the case. The first `await` prevented the issue with the blocked UI. – hellouworld Nov 28 '19 at 07:48
0

The awaits only allow you to wait for the operation/Task to be completed. So answering your question, you don't need more than one. You can save all Tasks into variables ( var t1 = ...GetCountAsync()....var t2=..InsertAsync..) and then call await Task.WhenAll(t1,t2,t3) to wait for all tasks to be completed. But it depends on the structure + behavior you want to give to your code. Maybe you need data to be persisted first and then use it on the next statement to call another method to pass it as a parameter, then you need to await for it first. Hope this helps

Zinov
  • 3,817
  • 5
  • 36
  • 70