ASP.NET MVC中异步编程的最佳实践是什么?

4
根据这篇文章,ASP.NET要求在控制器的异步操作中使用相同的SynchronizationContext,否则会阻塞运行线程。作者得出结论,我们应该使用以下两种方法以防止线程死锁成为最佳实践:
  1. 在你的“库”异步方法中,尽可能地使用ConfigureAwait(false)。
  2. 不要阻塞任务;始终使用异步方法。

    注意:最好应用两种最佳实践。其中任意一种都可以防止死锁,但必须同时应用才能实现最大的性能和响应能力。

但是,微软为什么要使用相同的SynchronizationContext呢?也许使用ConfigureAwait(false)会导致例如不可预测的行为或性能问题等。因此,在任何可能的情况下都使用ConfigureAwait(false)真的是一个好习惯吗?
2个回答

3

ASP.NET要求在控制器中使用相同的SynchronizationContext进行异步操作,否则会阻塞运行线程。

我不太确定这个陈述的意思。ASP.NET并不需要“相同”的同步上下文。为了让您在HttpContext中,您需要那个SynchronizationContext

但是,微软使用相同的SynchronizationContext有什么目的呢?

我建议您阅读It's All About SynchronizationContext,以便更好地了解同步上下文的大局。基本上,它是一种机制,允许您在特定线程上发布继续执行。例如,它可以用于将工作传回UI应用程序中的UI消息循环线程。

也许使用ConfigureAwait(false)会导致例如不可预测的行为或性能问题,因为它禁用了相同的同步上下文限制

相反地,将工作重新调度到同步上下文确实会有(非常小的)开销,因为请求需要使用抽象的 SynchronizationContext.Post 发布回所需的线程和上下文。通过使用 ConfigureAwait(false),您可以“节省”这些时间,并在分配任何线程上继续执行。
因此,无论何时可能,使用 ConfigureAwait(false) 确实是一个好习惯吗?
这样做的主要原因是避免死锁。当有人调用 Task.ResultTask.Wait 而不是使用 await 进行异步等待时,您会遇到经典的死锁场景,其中同步上下文正在尝试将继续返回到线程上,但它当前被阻塞,因为 ResultWait 是阻塞调用。另一个原因是您得到的轻微性能开销。
编辑:
为什么 Microsoft 不将此方法封装在 Task 类中?看起来 ConfigureAwait(false) 只有优点。是否有任何缺点?
让我们想象以下场景:您执行一个返回Task的方法,并使用await进行等待,紧接着您更新了一些UI元素。现在,对于您来说,哪种方式更自然?您是否希望隐式返回到之前的“环境”(UI线程),还是希望必须显式指定要返回到同一环境?我认为前者对用户来说更自然。例如:
public Task FetchAndReturnAsync(string url)
{
    var httpClient = new HttpClient();
    return httpClient.GetAsync(url);
}

你这样调用它:
var awesomeUiResult = await FetchAndReturnAsync("http://www.google.com");
textBox.Text = awesomeUiResult;

你认为应该发生什么?在等待后更新文本框更自然,还是因为不在原始上下文中而失败?这是问题。

缺点是您没有返回“同步上下文”,这可能会让最终用户感到惊讶。我将编辑我的答案进行解释。 - Yuval Itzchakov
我已经读了你的编辑一百遍,但是并没有完全理解你的意思:“执行一个返回Task的方法”,这个方法在哪里被调用了?它是在UI(操作方法)线程中吗? - dyatchenko
好的。谢谢你的例子。你的意思是说我们不应该在不同的上下文中更新文本框,因为这可能会导致异常(对于WinForms)? - dyatchenko
@dyatchenko 我的意思是,在等待方法完成后,更新文本框会更自然,就像同步代码一样async-await 的一般目的是简化异步编程并使其易于使用开发人员。 - Yuval Itzchakov
1
是的,你说得对,我意思也是这个。非常感谢!我想我已经得到了我的问题的答案。 - dyatchenko
显示剩余5条评论

2

ASP.NET要求在控制器中异步操作使用相同的SynchronizationContext,否则会阻塞运行线程。

有点像,但不完全正确。ASP.NET为每个传入请求创建一个,而此上下文根本没有绑定到特定的线程。但是,一次只能有一个线程进入上下文,因此如果第二个线程尝试在已经有一个线程进入时进入它,则它将阻塞该线程。

微软使用相同的SynchronizationContext的目的是什么?

ASP.NET的管理每个请求的数据,例如HttpContext.Current、区域设置和用户标识。

在任何可能的地方都使用ConfigureAwait(false)真的是好习惯吗?

作为一般规则,是的,特别是对于通用库。在ASP.NET上,ConfigureAwait(false)并没有带来太多好处 - 在某些情况下,略微提高性能。它可以用于避免我在文章中提到的死锁,但更好的做法(特别是在ASP.NET上)是始终使用async


好的,明白了。非常感谢Stephen!但是为什么在控制器的构造函数中无法使用异步操作呢?我已经在另一个问题中询问过了:https://dev59.com/Tl4b5IYBdhLWcg3wrzfZ - dyatchenko
@dyatchenko:在内部,DownloadStringTaskAsync以非常类似于async void的方式启动异步操作。 - Stephen Cleary
但是为什么我们可以在操作方法中使用 DownloadTaskAsync 而没有任何问题呢? - dyatchenko
因为操作方法(异步)等待它完成。构造函数不会这样做。ASP.NET看到异步操作尚未完成,因此会抛出该异常。 - Stephen Cleary

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