异步等待 - 我是否过度使用了它?

13

最近,我对在Web API项目中实现async-await模式的方式产生了疑问。我读到过async-await应该是“all the way”,这也是我所做的。但似乎这一切都变得多余了,我不确定自己是否正确地做了这件事。我有一个控制器调用了一个仓储库,然后它调用了一个数据访问(Entity Framework 6)类——“全部异步”。关于这个问题,我看到了很多相互矛盾的东西,希望能弄清楚。

编辑:被引用的重复帖子是一篇好文章,但并不够特定,不能满足我的需求。我包括了代码以说明问题。似乎很难在这个问题上得出决定性的答案。如果我们可以把async-await放在一个地方,让.NET处理剩下的部分,那就太好了,但我们不能这样做。那么,我是否过度使用了它,或者它并不像看起来那么简单。

以下是我的代码:

控制器:

public async Task<IHttpActionResult> GetMessages()
{
    var result = await _messageRepository.GetMessagesAsync().ConfigureAwait(false);
    return Ok(result);
}

存储库:

public async Task<List<string>> GetMessagesAsync()    
{
    return await _referralMessageData.GetMessagesAsync().ConfigureAwait(false);
}

数据:

public async Task<List<string>> GetMessagesAsync()
{           
    return await _context.Messages.Select(i => i.Message).ToListAsync().ConfigureAwait(false);
}

3
@SpacemanSpiff: 主观性不是一个有效的标准。如果它对 Stack Overflow 来说过于主观,那么对于 Programmers 来说也可能过于主观。请阅读 http://programmers.stackexchange.com/help/on-topic 以了解实际上会讨论哪些话题。 - Robert Harvey
2
实际上,我并不确定你在这里问什么。你能让你的问题更具体一些吗? - Robert Harvey
1
@BigDaddy 也许你应该看一下这个问题:Async all the way down?Async all the way down issue(看起来你的模式被称为“return await”模式)。 - Matias Cicero
1
只是为了给这里带来更多的矛盾信息,这里有两篇非常反对的文章:https://dev59.com/ml8f5IYBdhLWcg3wD_Av#25087273,https://dev59.com/eGcs5IYBdhLWcg3wgkMQ#12796711。本质上它们说异步在ASP.NET中几乎总是浪费时间。重要的是:我实际上解释了*为什么*而不仅仅陈述一些教条。请注意,异步现在非常流行,经常会没有理由地给出建议。 - usr
2
@BigDaddy我建议在合适的地方使用异步IO(请参阅我的帖子以了解何时是适当的)。在这里,它是一个福利,在其他任何地方都会成为障碍。理解异步IO有助于哪些方面和原因。令人惊奇的是,开发人员在没有注意到自己没有从中获得任何好处的情况下,会将累积的痛苦带给自己。程序员应该是逻辑思维者! :) - usr
显示剩余11条评论
2个回答

12
很希望我们可以把异步等待放在一个地方,让 .net 处理其余部分,但我们做不到。那么,我是过度了还是这并不简单呢。
如果能更简单就好了。
示例仓库和数据代码没有太多实际逻辑(在 await 之后也没有),所以它们可以简化为直接返回任务,正如其他评论者所指出的那样。
顺便说一下,示例仓库存在常见的仓库问题:什么都不做。如果你真实世界的仓库类似,你的系统可能有一个多余的抽象层次。请注意,Entity Framework 已经是通用的工作单元仓库。
但是关于一般情况下的 async 和 await,代码通常在 await 后还有工作要做。
public async Task<IHttpActionResult> GetMessages()
{
  var result = await _messageRepository.GetMessagesAsync();
  return Ok(result);
}

请记住,asyncawait只是用于连接回调的花哨语法。没有更简单的方法来异步表达此方法的逻辑。曾经有一些实验,例如推断await,但它们都被废弃了(我写了一篇博客文章描述了为什么async/await关键字具有所有这些“累赘”)。

而且对于每个方法来说,这些累赘都是必要的。每个使用async/await的方法都在建立自己的回调。如果回调不是必要的,那么该方法可以直接返回任务,避免使用async/await。其他异步系统(例如JavaScript中的Promise)也有相同的限制:它们必须一直异步执行。

可以在概念上定义一个系统,其中任何阻塞操作都会自动产生线程。我反对这样的系统的主要论据是它会有隐式的可重入性。特别是在考虑第三方库更改时,自动产生线程的系统将无法维护。最好在API的签名中明确异步性(即,如果返回Task,则为异步)。现在,@usr提出了一个很好的观点,即你可能根本不需要异步性。如果例如,你的Entity Framework代码正在查询单个SQL Server实例,则几乎可以肯定地是真的。这是因为async在ASP.NET上的主要优点是可扩展性,如果你不需要(ASP.NET部分的)可扩展性,则不需要异步性。请参阅我的MSDN文章async ASP.NET中的“不是万能药”部分。
然而,我认为也可以提出“自然API”的论点。如果一个操作是自然异步的(例如基于I/O),则它最自然的API是异步API。相反,自然同步操作(例如基于CPU)最自然地表示为同步API。自然API的论点对于库来说最为强大——如果你的存储库/数据访问层是自己的dll,旨在在其他(可能是桌面或移动)应用程序中重复使用,则它一定应该是异步API。但如果(更可能的情况是)它是针对不需要扩展的此ASP.NET应用程序的特定API,则没有必要将API设置为异步或同步。
但是关于开发者体验,有一个好的双重反驳。许多开发人员根本不知道如何使用 async;那么代码维护者会不会搞砸它呢?该论点的另一方面是,围绕 async 的库和工具仍在逐渐完善。最值得注意的是,在异常跟踪时缺乏因果堆栈(顺便说一下,我写了一个 library 来帮助解决这个问题)。此外,ASP.NET 的某些部分不兼容 async,尤其是MVC过滤器和子操作(他们正在用 ASP.NET vNext 来解决这两个问题)。对于异步处理程序,ASP.NET 在超时和线程中止方面具有不同的行为——这使得学习曲线更加陡峭。

当然,反反驳的论点是,应该通过培训来教育落后的开发人员,而不是限制可用的技术。

简而言之:

  • async 的正确使用方式是“全程异步”。特别是在 ASP.NET 上,这一点不太可能很快改变。
  • 是否使用 async ,以及是否有帮助,取决于您和应用程序的场景。

3
开发体验:你也无法对异步IO进行分析。堆栈上没有任何东西,似乎没有人在做任何事情。 - usr
1
还有别忘了本地化:异步任务可能会被不同的线程接管,而线程的 UI 文化可能会不同。我们在一个被多个国家本地化访问的系统中遇到了这个问题。 - NibblyPig
感谢您的深入评论。但我仍然感到困惑。您写道:“如果不需要回调函数,那么方法可以直接返回任务,避免使用async/await”,然后又写道:“正确的异步方式是“一路走到底””。对我来说,这些观点似乎相互矛盾。我能够将我的控制器保持异步,并让存储库和数据类返回Task<>吗?请澄清一下。 - Big Daddy
当我使用“全程异步”这个短语时,我的意思是每个调用异步方法的方法都应该是异步的。这里的“异步”是指“返回任务”,而不是“使用async关键字”。async关键字只是一种实现细节,它只是定义返回任务的一种方式。直接返回任务是另一种定义返回任务的方法。其他方法包括TaskFactory.FromAsyncTaskCompletionSource<T> - Stephen Cleary

7
public async Task<List<string>> GetMessagesAsync()    
{
    return await _referralMessageData.GetMessagesAsync().ConfigureAwait(false);
}

public async Task<List<string>> GetMessagesAsync()
{           
    return await _context.Messages.Select(i => i.Message).ToListAsync().ConfigureAwait(false);
}

如果你只调用异步方法的尾调用,那么你实际上不需要使用await
public Task<List<string>> GetMessagesAsync()    
{
    return _referralMessageData.GetMessagesAsync();
}

public Task<List<string>> GetMessagesAsync()
{           
    return _context.Messages.Select(i => i.Message).ToListAsync();
}

唯一失去的就是一些堆栈跟踪信息,但这些信息很少有用。删除await,然后不再生成处理等待的状态机,而是直接将所调用方法产生的任务传递回调用方法,并且调用方法可以await它。

现在有时候可以将方法内联,或者对它们进行尾递归优化。

如果相对简单,我甚至会把非基于任务的路径转换为基于任务的路径:

public async Task<List<string>> GetMeesagesAsync()
{
   if(messageCache != null)
     return messageCache;
   return await _referralMessageData.GetMessagesAsync().ConfigureAwait(false);
}

Becomes:

public Task<List<string>> GetMeesagesAsync()
{
   if(messageCache != null)
     return Task.FromResult(messageCache);
   return _referralMessageData.GetMessagesAsync();
}

然而,如果你在任何时候需要任务的结果来进行进一步的工作,那么使用 await 是最好的方法。


优秀的解释。 - Big Daddy
2
值得注意的是,这仍然是完全异步的,只是不是“完全异步”了 ;) - Jon Hanna
所以异步“一路向下”意味着调用返回类型为Task<>的方法。不需要使用async Task<>。我是否正确理解了您的意思?我猜测顶层方法,例如我的控制器方法,应该是async/await,因为它正在等待其他方法返回数据。 - Big Daddy
是的,asyncawait是达到目的的手段,但最终的目的是一个在适当时候让出处理器的Task<>,有时没有它们会更简单。然而,有时使用它们会简单得多。对于您的控制器,您可以编写一些代码来创建一个可等待的Task<ActionResult>以另一种方式返回,但是在.GetMessagesAsync()上进行await,然后从中创建ActionResult既是一种非常简单的方法,也是一种轻松处理许多潜在复杂性的方法。 - Jon Hanna
你和Stephen Cleary的回答都很出色。我喜欢你在回答中包含了代码。选择最佳答案是一个艰难的决定,但我会选择这个。 - Big Daddy

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接