【问题标题】:Why am I required to await a call when it's already been await'ed (.NET Core 2.2)为什么我需要等待已经等待的呼叫(.NET Core 2.2)
【发布时间】:2019-12-11 05:40:03
【问题描述】:

我有返回等待成员视图模型的服务方法。

public async Task<MemberVm> GetMember(Guid id)
{
  Task<Member> output = Context.Members
    .SingleOrDefaultAsync(e => e.Id == id);

  return await output != null
    ? new MemberVm(output)
    : null;
}

由于new MemberVm(output),这无法编译。相反,计算机要求我做new MemberVm(await output)。如果它是一个简单的 return 语句,我会理解它,但在这种情况下,它在评估条件表达式时已经被等待。对我来说,这似乎是伪代码。

if(await output != null)
  return await-again-but-why new MemberVm(output)
else
  return null;

是我做错了还是只是语言语法的意外和不幸后果?

【问题讨论】:

  • 我很确定你早上问过同样的事情。答案还是一样 - 等待 task 得到结果。 output 是一个任务,而不是操作的实际输出。使用Member output= await Context.Members.SingleOrDefaultAsync(e=&gt;e.Id==id)
  • 另外,output 永远不会是 null,因为 SingleOrDefaultAsync(或框架中的任何异步调用)永远不会返回空值。只有任务的结果才能产生null 值。
  • default(MemberVm) 不为空?
  • 即使在构造函数中等待调用,也不能等待 null。
  • @fildor 哦,好吧,是的,我知道,我想我只是对 output 在这种情况下的含义感到困惑。

标签: c# asynchronous async-await asp.net-core-2.2 .net-core-2.2


【解决方案1】:

如果您还没有阅读过async..await 的工作原理,您可能应该更好地推理一下;但是这些关键字的主要作用是触发将您的原始代码自动重写为继续传递样式。

基本上发生的事情是将您的原始代码转换为:

public Task<MemberVm> GetMember(Guid id)
{
    Task<Member> output = Context.Members
        .SingleOrDefaultAsync(e => e.Id == id);
    return output.ContinueWith((Task<Member> awaitedOutput) => 
        awaitedOutput.Result != null ? new MemberVm(output.Result) : null);
}

原始的output 变量保持不变,等待的结果(可以这么说)在可用时传递给延续。由于您没有将其保存到变量中,因此在首次使用后您将无法使用它。 (这是我称为 awaitedOutput 的 lambda 参数,如果您自己不将等待的输出分配给变量,它实际上可能会是 C# 编译器生成的乱码。)

在您的情况下,将等待的值存储在变量中可能是最简单的

public Task<MemberVm> GetMember(Guid id)
{
    Member output = await Context.Members
        .SingleOrDefaultAsync(e => e.Id == id);
    return output != null
        ? new MemberVm(output)
        : null;
}

您也可以直接在await 下的代码中使用output.Result,但这并不是您真正应该做的事情,而且有点容易出错。 (如果您出于某种原因无意中将output 重新分配给了不同的任务。这将导致整个线程变为Wait(),我猜它会冻结。)


至关重要的是,说“一个已经在等待的电话”没有任何意义。在后台,等待不是您对呼叫或任务执行的事情;这是对编译器的指令,以获取等待之后的所有代码,将其打包到一个闭包中,将其传递给Task.ContinueWith(),然后立即返回新任务。也就是说:await 本身不会导致等待调用结果,它会导致等待代码注册为回调,以便在结果可用时调用。如果你等待一个结果已经可用的任务,所有的变化就是这个回调将被更快地调用。

实现异步的方式是,在您需要等待调用完成的每个点,控制权都会返回到代码外部的某个事件循环。这个事件循环监视“从外部”到达的东西(例如,一些 I/O 操作完成),并唤醒任何等待这个的延续链。当您多次await 同一个Task 时,所发生的只是它处理了几个这样的回调。

(假设,是的,编译器还可以转换代码,以便在等待之后,原始变量名称引用新值。但是有很多原因我认为它没有以这种方式实现 - a变量更改中间函数在 C# 中是史无前例的,并且会令人困惑;而且总体而言,它似乎更复杂,更难推理。)


在这里进行一个有希望的说明性切线:我相信当您在同一 async 函数中两次 await 一个任务时会发生什么:

  1. 执行到达第一个等待,一个回调(其中包含创建并传递给ContinueWith(),控制返回到顶层。
  2. 一段时间后,调用结果可用,回调最终以结果作为参数调用。
  3. 第一个回调执行到第二个等待,创建另一个回调并传递给ContinueWith()
  4. 由于结果已经可用,可能会立即调用第二个回调,然后您的函数的其余部分将运行。

如您所见,两次等待同一个任务在事件循环中几乎是无意义的绕道。如果您需要任务的值,只需将其放入变量中即可。根据我的经验,很多时候您最好立即await 调用任何async 函数,并将此语句尽可能靠近使用任务结果的位置。 (因为 await 之后的任何代码在结果可用之前不会运行,即使它没有使用该结果。)例外情况是,如果您有一些代码需要在开始调用之后运行,但在使用结果之前,出于某种原因,您不能在通话开始之前运行。

【讨论】:

  • 它帮助我把它想象成传统的异步,你会有“StartAsyncCall”,等待之后的一切都进入“EndAsyncCall”。
  • 是的。它也类似于人们可能熟悉的 Javascript Promise 链接。但是,async..await 所做的真正关键是将使用熟悉的命令式控制流编写的代码重组为使用遗留异步或 Promises 时必须手动编写的代码。在 C# 中,这个想法的边缘有很多东西,比如让循环工作和调试它,以及能够在与触发它的位置不同的线程上安排延续,但它们并不是语言特性的真正含义关于。
  • 这是一个非常详尽且内容丰富的答案。我接受了@Fildor 的答案,因为它更短,并且特别指出了我跌跌撞撞的地方。但是,您为我所做的努力在更广泛的方面对我有益,我相信对其他人也是如此。所以我会做一些像小气鸭(几乎)从未做过的事情并赏金你。你的回答值得。
  • 但是你必须拿着你的小马来获得赏金 - 它说两天。我会尽量记住,但如果我不记得,请给我发送哔声。
【解决方案2】:

了解编译器标记是类型问题的问题很重要。 MemberVm 构造函数采用 Member 参数,但您的 output 变量的类型为 Task&lt;Member&gt;。编译器真的不希望您再次等待 Task,但这是从 Task 中提取结果并使类型工作的最常用方法。重写代码的另一种方法是更改​​ output 的类型:

Member output = await Context.Members.SingleOrDefaultAsync(e => e.Id == id);

现在您可以将output 直接传递给MemberVm 构造函数,因为您已经保存了第一个等待的结果。

【讨论】:

    【解决方案3】:

    这里已经有正确的答案,但解释远比必要的复杂。 await 关键字在任务完成之前保持执行解包任务(即Task&lt;Member&gt; 变成了Member)。但是,您并没有坚持那个展开部分。

    第二个output 仍然是Task&lt;Member&gt;。它现在已经完成,但它没有被解包,因为你没有保存它的结果。

    【讨论】:

    • 是的,一旦@fidor 用箭头显示,我马上就看到了。我不敢相信我以前怎么会错过。不知何故,在我的脑海中,等待的东西使他们解开并重新输入对象。现在我把它拼出来是没有意义的,但今天早上我的大脑所持有的甜蜜真相。愚蠢的...
    【解决方案4】:

    它无法编译,因为 outputTask,而不是 Member

    这可行:

    public async Task<MemberVm> GetMember(Guid id)
    {
      Member member = await Context.Members
        .SingleOrDefaultAsync(e => e.Id == id);
    
      return member != null
        ? new MemberVm(member)
        : null;
    }
    

    这不是:

    Task<Member> output = Context.Members
                                 .SingleOrDefaultAsync(e => e.Id == id);
    
    return await output != null // <= "await output" is null or a Member instance
        ? new MemberVm(output)  // "output" is always a Task<Member>
        : null;
    

    通过写await output“输出”本身不会被等待的结果所取代。它仍然是对您在上面创建的任务的相同引用。


    无关:我不建议返回 null。我想我会让MemberVM 处理使用null 进行设置,或者如果这强烈表明应用程序代码或数据库一致性有问题,则抛出异常。

    【讨论】:

    • 哦,我想我开始明白我哪里出错了。只是为了验证我是否正确。我可以使用new MemberVm(output.Value),一旦我确定由于条件语句而等待它。正确的? (当然,不建议这样做,这只是一个试探我是否正确的示例。)另外,感谢您在返回null 时的评论。我会看看如何在我的项目中重构代码。
    【解决方案5】:

    这行得通吗?

    MemberVm 的构造函数是什么样的?

    public async Task<MemberVm> GetMember(Guid id)
    {
      var output = await Context.Members
        .SingleOrDefaultAsync(e => e.Id == id);
    
      if (output == null)
         return null;
    
      return new MemberVm(output);
    }
    

    MemberVm 的构造函数似乎没有在其构造函数中使用 Task 参数(尽管没有看到代码,我无法确定)。相反,我认为构造函数只需要一个常规的 MemberVm 参数,因此通过评估 Context.Members... 在其他任何事情之前调用,应该有助于解决你正在发生的事情。如果没有,请告诉我,我们会解决的。

    【讨论】:

    • MemberVm 的构造函数采用 Member,而不是 Task。所以我知道需要等待 output 才能获得任务的价值。但我认为,可能是错误的,一旦我们对任务执行了等待,从而获取它的值,就不需要重新等待,因为值已经存在,预先等待。也许我在这方面弄错了?
    猜你喜欢
    • 2018-08-08
    • 2015-01-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-07-12
    • 1970-01-01
    • 2018-04-27
    相关资源
    最近更新 更多