异步等待方法在调用控制器操作方法时无法并行工作

时间:2018-08-16 07:31:20

标签: c# asp.net-mvc async-await

我有一个简单的控制器,分别具有同步和异步两种方法Index和IndexAsync。我尝试做与此article中写的相同的操作。但是出了点问题,我得到12分而不是5秒的结果,为什么呢?

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;
    }

}

结果:

enter image description here

3 个答案:

答案 0 :(得分:4)

许多人对await的行为模棱两可。尽管有这个名字,他们还是以某种方式相信它开始。真相 1

await在其右侧有一些表达。它不在乎该表达式是什么,它如何工作,等等。它所关心的只是表达式将产生 awaitable 2 。到目前为止,您将遇到的最常见的等待对象是TaskTask<T>

然后await完成简单的工作-这件事已经完成了吗?如果是这样,我们将得到结果(如果有),然后继续。 如果不是,那么我们将无法取得进一步的进展。如果其他人在此期间可以充分利用我们的线程,我们将让这种情况发生。待完成的事情完成后,我们安排了恢复执行包含await的方法。


我只是重申-await不在乎如何为什么创建了等待的对象。那是别人的工作-在这里,它是由async机器完成的,该机器改变了您的Delay方法。但是await不在乎该方法是否标记为async(这是该方法的实现详细信息,尽管出现在其中,但不是其签名的一部分),只是它承诺会返回一个“热” Task-已经运行的那个。


1 我认为这主要是因为有些人已经学会了近似相等async ≅ parallelism ≅ using threads。从来都不是真的,但这就是我们一直在努力纠正的问题。因此,他们认为“ async必须意味着我们正在创建线程”

2 的确,在您链接的文章中,Rawling已经解决的问题那里表达式指的是以前由初始化的变量调用Task返回方法。是什么使您自己的代码与众不同。

答案 1 :(得分:3)

在本文中,代码启动所有三个任务,然后唤醒所有三个任务

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;

您的代码开始并依次等待每个任务

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

答案 2 :(得分:0)

进行以下代码修改,以使Async调用一起执行:

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

Task.WhenAll将提供代表Task,当提供的集合中的所有任务完成执行(成功/失败)时,代表await将结束。因此,我们只是task代表result并获得了必要的行为,尽管它仍然可能无法保证预期的5秒钟。 int[]将是Delay,其中将包含每次调用Delay方法的值。

在您的情况下,await Task.Delay(1000 * sec);方法负责使用Delay启动任务,因此,如果您按已接受的答案所示调用await,则分别{ {1}},它们仍然可以并行执行,但让我们假设,您不确定或Delay方法如下:

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

然后使用await Task.WhenAll(...)变得很重要,因为它将启动尚未开始的任务,否则将仅等待它们完成。否则,使用接受的答案将不会获得好处。


有关C {6.0规范指南中的await作为常规信息的更多信息:

可等待的表达式

await表达式的任务必须是可等待的。如果满足以下条件之一,则可以等待表达式t:

  • t具有动态编译时间类型
  • t具有一个名为GetAwaiter的可访问实例或扩展方法,该方法或方法无参数且无类型参数,并具有以下所有条件的返回类型A:

    • A实现接口System.Runtime.CompilerServices.INotifyCompletion(以下称为 INotifyCompletion为简洁起见
    • A具有bool类型的可访问且可读的实例属性IsCompleted
    • A有一个可访问的实例方法GetResult,其中没有参数且没有类型参数。

GetAwaiter方法的目的是为任务获取等待者。 A型称为await表达式的awaiter类型。

IsCompleted属性的目的是确定任务是否已完成。如果是这样,则无需暂停评估。

INotifyCompletion.OnCompleted方法的目的是签署任务的“继续”;即,任务完成后将被调用的委托(类型为System.Action)。

GetResult方法的目的是在任务完成后获得任务的结果。

此结果可能是成功完成,可能带有结果值,或者可能是GetResult方法抛出的异常。


等待表达式的分类

表达式await t的分类方式与表达式(t).GetAwaiter().GetResult()相同。因此,如果返回类型为GetResult is void,则await_expression被归为空。如果它具有non-void return type T,则await_expression被归类为类型T的值。


等待表达式的运行时评估

在运行时,表达式await t的计算如下:

  • 通过计算表达式(t).GetAwaiter()获得一个等候者a
  • 通过计算表达式(a)获得布尔b。IsCompleted
  • 如果b为false,则评估取决于a是否实现了接口 System.Runtime.CompilerServices.ICriticalNotifyCompletion(为简便起见,以下简称为ICriticalNotifyCompletion)。此检查在绑定时完成;即在运行时,如果a的编译时类型为dynamic,否则在编译时。令r表示恢复委托(迭代器):

    • 如果a不实现ICriticalNotifyCompletion,则表达式 (a as (INotifyCompletion)).OnCompleted(r)被评估。
    • 如果a实现了ICriticalNotifyCompletion,则表达式 (a as (ICriticalNotifyCompletion)).UnsafeOnCompleted(r)被评估。
    • 然后暂停评估,并将控制权返回给异步函数的当前调用方。
  • 或者紧接(if b was true )之后,或者稍后调用恢复委托(if b was false )时,都会对表达式(a).GetResult()进行求值。如果返回值,则该值是 await_expression。否则结果一无是处。

  • 接口方法INotifyCompletion.OnCompletedICriticalNotifyCompletion.UnsafeOnCompleted的等待者实现应导致委托r最多被调用一次。否则,封闭的异步函数的行为是不确定的。