ASP.NET Core SynchronizationContext

原文链接:https://blog.stephencleary.com/2017/03/aspnetcore-synchronization-context.html

为什么 ASP.NET Core 中没有 SynchronizationContext?

退后一步讲,一个很好的问题是为什么在 ASP.NET Core 中移除 AspNetSynchronizationContext。微软开发团队内部是怎么讨论的我不知道,我想了有两个理由:性能和简单化。第一个方面就是考虑性能。

当一个异步处理程序在 ASP.NET 恢复执行时,接着会入队列到请求上下文。然后它又必须等待其他之前已经入队列的延续任务完成(一次只运行一个)。当它准备运行时,一个线程池线程被消费,进入到请求上下文,并且恢复执行处理程序。“重复入队列” 这个请求上下文会涉及到很多内部工作,例如设置 HttpContext.Current 和当前线程身份以及文化信息。

使用无上下文 ASP.NET Core 方法,当异步处理程序恢复执行时,一个从线程池产生的线程会继续执行。这个上下文会避免排队,请求上下文避免了重复进入队列 。另外,async / await 机制在无上下文场景下进行了高度优化。异步请求只需要做更少的工作。

简单是这个决定(无上下文化)的另一个方面。AspNetSynchronizationContext 工作的很好,但是这里还有一些棘手的部分,尤其是在身份验证管理方面

OK,所以这里没有 SynchronizationContext。那么对于开发者意味着要做什么呢?

你可以阻塞异步代码——但是不应该这么做

首先也是最明显的结果就是,await 没有捕获上下文。这也意味着阻塞异步代码将不会引起死锁。你可以使用 Task.GetAwaiter().GetResult() (或者是 Task.Wait 或者是 Task<T>.Result)都不惧死锁。

然而,你不应该这么做。因为在你的异步代码被阻塞的时候,你正在放弃异步代码带来的每一个好处。一旦阻塞线程,异步处理程序的增强可伸缩性就会无效。

然而,不幸的是,在 ASP.NET 场景里还遗留两个同步操作:ASP.NET MVC 过滤器以及子控制器。然而,在 ASP.NET Core 中,整个管道都是完整的异步;包括过滤器以及组件的展示都是异步的。

总之,你应该尽量的在所有地方都使用 async;但是如果你需要,你可以不顾危险的阻塞它。

你无需用到 ConfigureAwait(false),但是在库中还是要用到

这从没有上下文,ConfigureAwait(false) 也就是没必要了。我们知道任何代码运行在 ASP.NET Core 是不需要显式避免上下文。事实上,ASP.NET Core 团队内部自己都抛弃使用 ConfigureAwait(false)

然而,我还是建议你在你的库中保留使用它 — 在应用程序中任何事情都有可能被重新启用。如果库中的代码也能在 UI 应用程序中运行,或者在遗留的 ASP.NET app ,或任何其他有上下文的地方,你都还是可以在库中使用 ConfigureAwait(false)

小心隐式的并发

这里有个主要的概念就是当从一个同步上下文到一个线程池线程(比如从遗留的 ASP.NET 到 ASP.NET Core)。

在老的 ASP.NET 的 SynchronizationContext 是一个实际的同步上下文,以为着也包含一个请求上下文,一次只能执行一个线程。那就是说,异步延续可能会在任何线程,但是一次只有一个。ASP.NET Core 不存在 SynchronizationContext ,所以 await 默认就是线程池上下文。所以在 ASP.NET Core 的世界中,异步可能延续在任何线程上,并且还可能是并发运行的。

作为一个例子,考虑这段代码,它下载了两个字符串并把他们放到一个列表中。这段代码在老旧的 ASP.NET 能很好的工作,因为请求上下文一次只允许执行一个延续。

private HttpClient _client = new HttpClient();

async Task<List<string>> GetBothAsync(string url1, string url2)
{
    var result = new List<string>();
    var task1 = GetOneAsync(result, url1);
    var task2 = GetOneAsync(result, url2);
    await Task.WhenAll(task1, task2);
    return result;
}

async Task GetOneAsync(List<string> result,string url)
{
    var data = _client.GetStringAsync(url);
    result.Add(data);
}

其中 result.Add(data) 这行只能够一次只一个线程执行,因为它是在请求上下文中执行的。

然而,这段相同的代码在 ASP.NET Core 是不安全的;特别要指出的是,result.Add(data) 这行可能一次被两个线程执行,在没有保护共享的变量 List<string> 的情况下。

这样的代码是很少见的;异步代码本质上是功能性的。因此,从异步方法很自然的返回一个结果而不是修改共享状态。然而,异步代码的质量也是多变的,并且毫无疑问,有一些代码没有充分的屏蔽平行度。