简体   繁体   中英

async await methods don't work parallel on calling controller action method

i have a simple controller with two methods Index and IndexAsync synchronous and asynchronous respectively. I try to do same as written in this article . But something going wrong , instead of 5 seconds result i get 12. How come , what's the reason ?

public class HomeController : Controller
{
    [HttpGet]
    public ActionResult Index()
    {
        return View();
    }

    [HttpGet]
    public async Task<string> IndexAsync()
    {
        Stopwatch stopwatch = new Stopwatch();
        stopwatch.Start();
        await Delay(3);
        await Delay(5);
        await Delay(4);
        stopwatch.Stop();
        return stopwatch.Elapsed.Seconds.ToString();
    }


    private async Task<Int32> Delay(int sec)
    {
        await Task.Delay(1000 * sec);
        return sec;
    }

}

the result:

在此处输入图片说明

Many people have a bad model in their heads of what await does. Despite it's name, they somehow come to believe that it starts things happening. Nothing could be further from the truth 1 .

await has some expression on its right-hand side. It doesn't care what that expression is, how it goes about its work, etc. All it cares about is that the expression is going to produce something that is awaitable 2 . By far the most common awaitables you'll encounter are Task and Task<T> .

And then await does its simple job - is this awaitable thing finished ? If so, we'll get the result (if any) and then carry on. If not , then we can't make any further progress. If someone else can make good use of our thread in the meantime, we'll let that happen. And when that awaitable thing does finish, we've arranged to resume execution of the method containing the await .


I will just reiterate - await doesn't care how or why that awaitable got created. That's someone else's job - here, it's being done by the async machinery that transformed your Delay method. But await doesn't care that that method is marked async (that's an implementation detail of that method, not part of its signature, despite where it appears), just that it promises to return a "hot" Task - one that's already running.


1 I think it's mostly because some people have learned the approximate equality async ≅ parallelism ≅ using threads . It was never really true but this is what we're constantly fighting to correct. And so they think " async must mean we're creating threads"

2 Indeed, in the article you linked to, and the issue already addressed by Rawling the expressions there referred to variables that had previously been initialized by calls to Task -returning methods. Which is what made your own code different.

In the article, the code starts all three tasks and then awaits all three tasks :

var contentTask = service.GetContentAsync(); // start task 1
var countTask = service.GetCountAsync();
var nameTask = service.GetNameAsync();

var content = await contentTask; // wait until task 1 is finished
var count = await countTask;
var name = await nameTask;

Your code starts and awaits each task in turn :

await Delay(3); // start task 1 and wait until it is finished
await Delay(5);
await Delay(4);

Make the following code modification, for the Async calls to be executed together not one after another :

var result = await Task.WhenAll(Delay(3),Delay(4),Delay(5));

Task.WhenAll will provide the representative Task , which will finish, when all the tasks in the collection supplied are finished execution (Success / Failure). So we just await the representative task and get the necessary behavior, though it still may not guarantee 5 sec as expected. result will be an int[] , which will contain value from each call to Delay method.

In your case, the Delay method takes the responsibility of beginning the Task, using await Task.Delay(1000 * sec); , therefore if you just call the Delay as shown by the accepted answer, then separately await , then they still get executed in parallel, but let's assume, you are not sure or the Delay method is as follows:

private Task Delay(int sec)
{
  return Task.Delay(1000 * sec);
}

then using await Task.WhenAll(...) becomes important, since it will start the tasks which are not started, otherwise will simply wait for them to finish. Otherwise using the accepted answer you will not receive the benefit.


More regarding await as general information, from the C# 6.0 specification guide:

Awaitable expressions

The task of an await expression is required to be awaitable. An expression t is awaitable if one of the following holds:

  • t is of compile time type dynamic
  • t has an accessible instance or extension method called GetAwaiter with no parameters and no type parameters, and a return type A for which all of the following hold:

    • A implements the interface System.Runtime.CompilerServices.INotifyCompletion (hereafter known as INotifyCompletion for brevity)
    • A has an accessible, readable instance property IsCompleted of type bool
    • A has an accessible instance method GetResult with no parameters and no type parameters.

The purpose of the GetAwaiter method is to obtain an awaiter for the task. The type A is called the awaiter type for the await expression.

The purpose of the IsCompleted property is to determine if the task is already complete. If so, there is no need to suspend evaluation.

The purpose of the INotifyCompletion.OnCompleted method is to sign up a "continuation" to the task; ie a delegate (of type System.Action ) that will be invoked once the task is complete.

The purpose of the GetResult method is to obtain the outcome of the task once it is complete.

This outcome may be successful completion, possibly with a result value, or it may be an exception which is thrown by the GetResult method.


Classification of await expressions

The expression await t is classified the same way as the expression (t).GetAwaiter().GetResult() . Thus, if the return type of GetResult is void , the await_expression is classified as nothing. If it has a non-void return type T , the await_expression is classified as a value of type T .


Runtime evaluation of await expressions

At runtime, the expression await t is evaluated as follows:

  • An awaiter a is obtained by evaluating the expression (t).GetAwaiter()
  • A bool b is obtained by evaluating the expression (a).IsCompleted
  • If b is false then evaluation depends on whether a implements the interface System.Runtime.CompilerServices.ICriticalNotifyCompletion (hereafter known as ICriticalNotifyCompletion for brevity). This check is done at binding time; ie at runtime if a has the compile time type dynamic , and at compile time otherwise. Let r denote the resumption delegate (Iterators):

    • If a does not implement ICriticalNotifyCompletion , then the expression (a as (INotifyCompletion)).OnCompleted(r) is evaluated.
    • If a does implement ICriticalNotifyCompletion , then the expression (a as (ICriticalNotifyCompletion)).UnsafeOnCompleted(r) is evaluated.
    • Evaluation is then suspended, and control is returned to the current caller of the async function.
  • Either immediately after (if b was true ) , or upon later invocation of the resumption delegate (if b was false ) , the expression (a).GetResult() is evaluated. If it returns a value, that value is the result of the await_expression. Otherwise the result is nothing.

  • An awaiter's implementation of the interface methods INotifyCompletion.OnCompleted and ICriticalNotifyCompletion.UnsafeOnCompleted should cause the delegate r to be invoked at most once. Otherwise, the behavior of the enclosing async function is undefined.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM