【发布时间】:2014-01-26 09:32:24
【问题描述】:
这个问题是由EF Data Context - Async/Await & Multithreading 触发的。我已经回答了这个问题,但没有提供任何最终解决方案。
最初的问题是那里有很多有用的 .NET API(例如 Microsoft Entity Framework 的 DbContext),它们提供了设计用于 await 的异步方法,但它们被记录为 不是线程安全的。这使得它们非常适合在桌面 UI 应用程序中使用,但不适用于服务器端应用程序。 [EDITED] 这实际上可能不适用于DbContext,这是微软的statement on EF6 thread safety,请自行判断。 [/EDITED]
还有一些已建立的代码模式属于同一类别,例如使用OperationContextScope 调用 WCF 服务代理(询问here 和here),例如:
using (var docClient = CreateDocumentServiceClient())
using (new OperationContextScope(docClient.InnerChannel))
{
return await docClient.GetDocumentAsync(docId);
}
这可能会失败,因为OperationContextScope 在其实现中使用了线程本地存储。
问题的根源是AspNetSynchronizationContext,它用于异步ASP.NET 页面,以使用来自ASP.NET 线程池的更少线程来满足更多HTTP 请求。使用AspNetSynchronizationContext,await 延续可以在与启动异步操作的线程不同的线程上排队,而原始线程被释放到池中并可用于服务另一个 HTTP 请求。 这极大地提高了服务器端代码的可扩展性。 该机制在It's All About the SynchronizationContext 中有详细描述,必读。因此,虽然不涉及并发 API 访问,但潜在的线程切换仍会阻止我们使用上述 API。
我一直在考虑如何在不牺牲可伸缩性的情况下解决这个问题。显然,让这些 API 恢复的唯一方法是为范围保持线程亲和性的异步调用可能受线程切换影响。
假设我们有这样的线程亲和力。无论如何,这些调用中的大多数都是 IO 绑定的 (There Is No Thread)。当一个异步任务挂起时,它发起的线程可用于为另一个类似任务的延续提供服务,该结果已经可用。因此,它不应该过多地损害可扩展性。这种做法并不是什么新鲜事,其实类似的单线程模型是successfully used by Node.js。 IMO,这是让 Node.js 如此受欢迎的原因之一。
我不明白为什么不能在 ASP.NET 上下文中使用这种方法。自定义任务调度程序(我们称之为ThreadAffinityTaskScheduler)可能会维护一个单独的“亲和单元”线程池,以进一步提高可伸缩性。一旦任务排队到这些“公寓”线程之一,任务内的所有await 延续将发生在同一个线程上。
以下是来自the linked question 的非线程安全 API 如何与此类 ThreadAffinityTaskScheduler 一起使用:
// create a global instance of ThreadAffinityTaskScheduler - per web app
public static class GlobalState
{
public static ThreadAffinityTaskScheduler TaScheduler { get; private set; }
public static GlobalState
{
GlobalState.TaScheduler = new ThreadAffinityTaskScheduler(
numberOfThreads: 10);
}
}
// ...
// run a task which uses non-thread-safe APIs
var result = await GlobalState.TaScheduler.Run(() =>
{
using (var dataContext = new DataContext())
{
var something = await dataContext.someEntities.FirstOrDefaultAsync(e => e.Id == 1);
var morething = await dataContext.someEntities.FirstOrDefaultAsync(e => e.Id == 2);
// ...
// transform "something" and "morething" into thread-safe objects and return the result
return data;
}
}, CancellationToken.None);
我继续使用 implemented ThreadAffinityTaskScheduler 作为概念证明,基于 Stephen Toub 的出色 StaTaskScheduler。 ThreadAffinityTaskScheduler 维护的池线程不是经典 COM 意义上的 STA 线程,但它们确实实现了 await 延续的线程关联(SingleThreadSynchronizationContext 负责)。
到目前为止,我已经将此代码作为控制台应用程序进行了测试,它似乎可以按设计工作。我还没有在 ASP.NET 页面中对其进行测试。我没有很多生产 ASP.NET 开发经验,所以我的问题是:
在 ASP.NET 中对非线程安全 API 的简单同步调用使用这种方法是否有意义(主要目标是避免牺牲可伸缩性)?
除了使用同步 API 调用或完全避免使用这些 API 之外,还有其他方法吗?
有没有人在 ASP.NET MVC 或 Web API 项目中使用过类似的东西并准备分享他/她的经验?
有关如何使用 ASP.NET 对这种方法进行压力测试和分析的任何建议都是 感激不尽。
【问题讨论】:
-
据我了解,“非线程安全”一词并不意味着您不能从不同的线程访问对象,只是必须以某种方式同步来自不同线程的访问。我认为允许
DbContext的控制在线程之间移动没有问题,只要在任何时候只有一个线程可以访问它。您引用的对象类型是 STA 对象,它们的处理方式非常不同。 -
@Aron,就像我说的,没有并发 API 访问,但 API 仍然可能失败,例如因为它使用线程本地存储。我自己没有在 ASP.NET 中使用过
DbContext/DataContext,但我相信链接的question 的 OP。 -
在this articleStephen Toub 中特别提到 async/await 设施正在流动 ExecutionContext,因此是 TLS,不是吗?
-
@G.Stoynev,不,
ExecutionContext不会自动传输整个 TLS 状态,它应该不如此。我已经解决了您的评论,并提供了更多详细信息here。
标签: c# asp.net .net task-parallel-library async-await