3

I know this question might be a bit trivial, but all the answers I find on the internet leave me confused. I'm aware with basic principles of how async/await works (how await asynchroniously waits for the task to complete not blocking the main thread), but I don't understand its real benefit, because it seems to me everything you do with async/await you can do using Task Paralel Library. Please consider this example, to better understand what I mean: Let's say I have a SuperComplexMethod that returns some value and I would like to execute it in parallel, meanwhile doing some other things. Normally I would do it this way:

internal class Program
{
    private static void Main()
    {
        //I will start a task first that will run asynchroniously  
        var task = Task.Run(() => SuperComplexMethod());

        //Then I will be doing some other work, and then get the result when I need it
        Console.WriteLine("Doing some other work...");
        var result = task.Result;
    }

    static string SuperComplexMethod()
    {
        Console.WriteLine("Doing very complex calculations...");
        Thread.Sleep(3000);
        return "Some result";
    }
} 

Here how I would have to do it using async/await:

internal class Program
{
    private static void Main()
    {
        var task = SuperComplexMethodAsync();

        Console.WriteLine("Doing some other work...");
        var result = task.Result;
    }

    //I have to create this async wrapper that can wait for the task to complete
    async static Task<string> SuperComplexMethodAsync()
    {
        return await Task.Run(() => SuperComplexMethod());
    }

    static string SuperComplexMethod()
    {
        Console.WriteLine("Doing very complex calculations...");
        Thread.Sleep(3000);
        return "Some result";
    }
}

As you can see in the second example in order to use async/await approach, I have to create a wrapper method that starts a task and asynchronously waits for it to complete. Obviously it seems redundant to me, because I can achieve the very same behavior without using this wrapper marked async/await.

Can you please explain me what is so special about async/await, and what actual benefits it provides over using tools of Task Parallel Library alone?

7
  • Just as an FYI: async/await is part of the Task Parallel Library. Commented Dec 3, 2017 at 14:43
  • 3
    In your example async\await is indeed useless. But that's just wrong example. Commented Dec 3, 2017 at 14:47
  • var t1= task1() ; var t2= task2() ;var t3= task3() ; var all = await task1()+ await task2() + await task3() - how would you do this without async/await ? very hard. Commented Dec 3, 2017 at 14:48
  • @RoyiNamir, I guess I would do something like this: t1.Run(); t2.Run(); t3.Run(); //do some other work neccessary Task.WaitAll(t1,t2,t3); var all = t1.Result + t2.Result + t3.Result. Commented Dec 4, 2017 at 6:48
  • You can easily get deadlock in UI-based applications with .Result property. async/await catch synchronization context for you and restores it, which TPL does not. Commented Dec 4, 2017 at 19:30

2 Answers 2

3

Arguably the main reason to use async/await is thread sparing. Imagine the following scenario (I'll simplify to make the point): a) you have a web application that has 10 threads available to process incoming requests; b) all requests involve I/O (e.g. connecting to a remote database, connecting to upstream network services via HTTP/SOAP) to process/complete; c) each request takes 2 seconds to process.

Now imagine 20 requests arrive at about the same time. Without async/await, your web app would start to process the first 10 requests. While this is happening the other 10 would just sit in the queue for 2 seconds, with your web app out of threads and hence unable to process them. Only when the first 10 complete would the second 10 begin to be processed.

Under async/await, the first 10 requests would instead begin tasks, and, while awaiting those tasks, the threads that were processing them would be returned to the web app to process other requests. So your web app would begin processing the second 10 almost straight away, rather than waiting. As each of the awaited tasks from the first 10 completes, the web app would continue processing the rest of their methods, either on a thread-pool thread or one of the web app's threads (which it is depends on how you configure things). We can usually expect in an I/O scenario that the I/O is by far the bulk of the duration of the call, so we can make a reasonable assumption that in the above scenario, the network/database call might take 1.9s and the rest of the code (adapting DTOs, some business logic, etc.) might take 0.1s. If we assume the continuation (after the await) is processed by a web app thread, that thread is now only tied up for 0.1 of the 2 seconds, instead of the full 2 seconds in the non async/await scenario.

You might naturally think: well I've just pushed the threads out of one pool of threads and into another, and that will eventually fill up too. To understand why this isn't really true in practise in truly async scenarios, you need to read There Is No Thread.

The upshot is that you are now able to concurrently process many more requests than you have threads available to process them.

You'll notice the above is focused on I/O, and that's really where async/await shines. If your web app instead processed requests by performing complex mathematical calculations using the CPU, you would not see the above benefit, hence why async/await is not really suited for nor intended for use with CPU-bound activities.

Before others jump in with all the exceptions to the rules (and there are some), I'm only presenting a vanilla simplified scenario to show the value of async/await in I/O-bound scenarios. Covering everything about async/await would create a very long answer (and this one is long enough already!)

I should also add that there are other ways to process web requests asynchronously, ways that pre-date async/await, but async/await very significantly simplifies the implementation.

--

Moving briefly to say a WinForms or similar app, the scenario is very similar, except now you really only have one thread available to process UI requests, and any time you hold onto that thread, the UI will be unresponsive, so you can use a similar approach to move long-running operations off the UI thread. In the UI scenario, it becomes more reasonable to perform CPU-bound operations off the UI thread as well. When doing this, a thread pool thread will instead perform that CPU work, freeing up the UI thread to keep the UI responsive. Now there is a thread, but at least it's not the UI one. This is generally called "offloading", which is one of the other primary uses for async/await.

--

Your example is a console app - there's often not a lot to be gained in that context, except for the ability to fairly easily (arguably more easily than creating your own threads) execute several requests concurrently on the thread pool.

Sign up to request clarification or add additional context in comments.

1 Comment

Thank you for your answer. From what you wrote, it seems that the only benefit async/await provides is an effective usage of Thread Pool, which is not always the issue, if the application does not depend on many threads (and by many I mean probably around 1000 or something, because your example with 10 was, as I understood, a simplified one), there is no need of using async/await. And speaking of WinForms I didn't really understand what you meant, because again I could run a separate task that updates UI independently of the main thread and then the UI would remain responsive.
0

When using async and await the compiler generates a state machine in the background

public async Task MyMethodAsync()
{
    Task<int> longRunningTask = LongRunningOperationAsync();
    // independent work which doesn't need the result of LongRunningOperationAsync can be done here

    //and now we call await on the task 
    int result = await longRunningTask;
    //use the result 
    Console.WriteLine(result);
}

public async Task<int> LongRunningOperationAsync() // assume we return an int from this long running operation 
{
    await Task.Delay(1000); // 1 second delay
    return 1;
}

so what happens here:

  1. Task longRunningTask = LongRunningOperationAsync(); starts executing LongRunningOperation

  2. Independent work is done on let's assume the Main Thread (Thread ID = 1) then await long running task is reached.

Now, if the longRunningTask hasn't finished and it is still running, MyMethodAsync() will return to its calling method, thus the main thread doesn't get blocked. When the longRunningTask is done then a thread from the ThreadPool (can be any thread) will return to MyMethodAsync() in its previous context and continue execution (in this case printing the result to the console).

A second case would be that the longRunningTask has already finished its execution and the result is available. When reaching the await longRunningTask we already have the result so the code will continue executing on the very same thread. (in this case printing result to console). Of course this is not the case for the above example, where there's a Task.Delay(1000) involved.

For More ... Refer :=>

Async/Await - Best Practices

Simplifying Asynchronous

2 Comments

Hope my answer will help to your development.
Thank you for the answer, but I don't think it answers my original question with the example I provided. I could rewrite your code using vanilla TLP and it would pretty much do the same work (please refer to the example I provided).

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.