【问题标题】:async await methods don't work parallel on calling controller action method异步等待方法在调用控制器操作方法时不能并行工作
【发布时间】:2018-08-22 22:34:52
【问题描述】:

我有一个简单的控制器,有两个方法 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;
    }

}

结果:

【问题讨论】:

  • 这必然会发生,因为您使用 await 一个接一个地进行异步调用,这将使其结果为 3 + 4 + 5 = 12 秒
  • 你需要的是Task.WhenAll,让它在5秒内完成
  • 请注意,您链接到的代码单独调用了包含延迟的方法并然后awaited 结果。跨度>
  • @Damien_The_Unbeliever 我尝试遵循article 中使用的方法。你能解释一下,我的代码和文章代码有什么区别吗?
  • var contentTask = service.GetContentAsync(); - 调用GetContentAsync 并在此过程中返回。然后通过调用其他异步方法开始另外两个异步操作,并且只有在之后他们才写await contentTask。而您立即 await 每个异步方法调用。

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


【解决方案1】:

对于await 所做的事情,很多人的头脑中都有一个糟糕的模型。尽管它的名字,他们不知何故开始相信它开始事情发生。事实并非如此1

await 的右侧有一些表达式。它不关心那个表达式是什么,它是如何工作的,等等。它只关心这个表达式将产生什么是可等待的2 .到目前为止,您会遇到的最常见的 awaitable 是 TaskTask&lt;T&gt;

然后await 完成了它的简单工作——这个等待的事情完成了吗?如果是这样,我们将得到结果(如果有)然后继续。 如果不是,那么我们就无法取得进一步的进展。如果其他人同时可以很好地利用我们的线程,我们会让这种情况发生。当等待的事情完成时,我们已安排恢复执行包含await 的方法。


我只是重申一下 - await 不在乎 如何为什么 创建 awaitable。那是别人的工作 - 在这里,它是由 async 机器完成的,它改变了你的 Delay 方法。但是await 并不关心该方法是否被标记为async(这是该方法的实现细节,而不是其签名的一部分,尽管它出现在哪里),只是它承诺返回一个“热”Task - 一个已经在运行的。


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

2确实,在您链接到的文章中,以及 Rawling 已经解决的问题中,表达式 there 引用了之前通过调用Task-返回方法。这就是使您自己的代码与众不同的原因。

【讨论】:

  • 谢谢,这是关于 await 运算符的非常有价值的评论
【解决方案2】:

在文章中,代码启动所有三个任务,然后等待所有三个任务

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

【讨论】:

    【解决方案3】:

    进行以下代码修改,使异步调用不会一个接一个地一起执行:

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

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

    在您的情况下,Delay 方法负责使用await Task.Delay(1000 * sec); 开始任务,因此如果您只是按照接受的答案所示调用Delay,然后分别调用await,那么他们仍然并行执行,但假设您不确定或延迟方法如下:

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

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


    更多关于 await 的一般信息,来自 C# 6.0 规范指南:

    等待表达式

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

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

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

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

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

    INotifyCompletion.OnCompleted 方法的目的是为任务注册一个“延续”;即任务完成后将调用的委托(System.Action 类型)。

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

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


    await 表达式的分类

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


    等待表达式的运行时评估

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

    • 通过评估表达式(t).GetAwaiter() 获得等待者a
    • 通过计算表达式 (a).IsCompleted 获得 bool b
    • 如果 b 为假,则评估取决于 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()。如果它返回一个值,则该值是 等待表达式。否则结果什么都没有。

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

    【讨论】:

    • 你能解释一下,我的代码和article代码有什么区别
    • 在您的代码中它是同步的,因为它将在每个await 语句处等待,只是由于await 它不会阻塞调用者,但Task.WhenAll 会一起去
    • 为什么要将任务插入列表?只需var result = await Task.WhenAll(Delay(3), Delay(4), Delay(5));
    • 两种方法都可以,没什么区别,我想在解释上简单一点
    • @ChayimFriedman 进行了您建议的更改,以使代码更清晰
    猜你喜欢
    • 2014-05-15
    • 2015-04-26
    • 1970-01-01
    • 1970-01-01
    • 2015-08-20
    • 1970-01-01
    • 2015-11-09
    • 2017-10-08
    • 1970-01-01
    相关资源
    最近更新 更多