此问题已由EF数据上下文- 异步/等待和多线程触发。我已经回答了一个,但是还没有提供任何最终解决方案。
最初的问题是,那里有很多有用的.NET API(例如Microsoft Entity Framework的API DbContext),它们提供了旨在与之一起使用的异步方法await,但据记录它们 不是线程安全的 。这使其非常适合在桌面UI应用程序中使用,但不适用于服务器端应用程序。 [编辑] 这可能实际上并不适用DbContext,这是Microsoft 关于EF6线程安全性的声明,请自行判断。 [/编辑]
DbContext
await
也有一些已建立的代码模式属于同一类别,例如使用OperationContextScope(在此处和此处询问)调用WCF服务代理,例如:
OperationContextScope
using (var docClient = CreateDocumentServiceClient()) using (new OperationContextScope(docClient.InnerChannel)) { return await docClient.GetDocumentAsync(docId); }
这可能会失败,因为OperationContextScope在其实现中使用线程本地存储。
问题的根源AspNetSynchronizationContext是在异步ASP.NET页中使用它来以更少的线程ASP.NET池线程来满足更多的HTTP请求。使用AspNetSynchronizationContext,await可以在与发起异步操作的线程不同的线程上排队一个延续,同时将原始线程释放到池中,并可以用于服务另一个HTTP请求。 这大大提高了服务器端代码的可伸缩性。 该机制在必须阅读的“关于同步上下文的全部内容”中进行了详细描述。因此,尽管 不 涉及 并发API访问 ,但是潜在的线程切换仍然阻止我们使用上述API。
AspNetSynchronizationContext
ASP.NET
我一直在考虑如何在不牺牲可伸缩性的情况下解决此问题。 显然,收回这些API的唯一方法是针对可能受到线程切换影响的异步调用范围 保持线程亲和性 。
假设我们具有这种线程相似性。无论如何,这些调用中的大多数都是受IO约束的(没有线程)。当异步任务挂起时,它所源自的线程可用于服务另一个类似任务的延续,该结果已经可用。因此,它不应过多地损害可伸缩性。这种方法并不是什么新鲜事物,实际上, Node.js 已 成功使用 了类似的单线程模型 。IMO,这是使Node.js如此受欢迎的原因之一。
我不明白为什么这种方法不能在ASP.NET上下文中使用。定制任务调度程序(我们称之为ThreadAffinityTaskScheduler)可以维护一个单独的“亲和公寓”线程池,以进一步提高可伸缩性。一旦任务已排队到那些“公寓”线程之一中,await任务内部的所有继续将在同一线程上进行。
ThreadAffinityTaskScheduler
这是链接问题中的非线程安全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);
基于Stephen Toub的出色表现,我继续进行并 作为概念证明进行了 实施ThreadAffinityTaskSchedulerStaTaskScheduler。ThreadAffinityTaskScheduler在经典的COM意义上,由其维护的池线程不是STA线程,但是它们确实实现了线程的await连续性(SingleThreadSynchronizationContext对此负责)。
StaTaskScheduler
SingleThreadSynchronizationContext
到目前为止,我已经将该代码作为控制台应用程序进行了测试,并且看起来可以按设计工作。我尚未在ASP.NET页面中对其进行测试。我没有大量的ASP.NET生产开发经验,所以我的问题是:
在ASP.NET中非线程安全的API的简单 同步 调用上使用这种方法是否有意义(主要目标是避免牺牲可伸缩性)?
除了使用同步API调用或完全避免这些APis之外,还有其他方法吗?
有没有人在ASP.NET MVC或Web API项目中使用过类似的东西,并准备分享他/她的经验?
任何有关如何使用ASP.NET进行压力测试和概要分析的建议都将受到赞赏。
实体框架将(应该)处理跨await点的线程跳转。如果没有,那是EF中的错误。OTOH OperationContextScope基于TLS,并且await不安全。
1.同步API维护您的ASP.NET上下文;这包括诸如用户身份和文化等在处理过程中通常很重要的东西。另外,许多ASP.NET API都假定它们在实际的ASP.NET上下文中运行(我并不是说仅使用HttpContext.Current;我是说实际上是假定它SynchronizationContext.Current是的一个实例AspNetSynchronizationContext)。
HttpContext.Current
SynchronizationContext.Current
2-3。我使用了直接嵌套在ASP.NET上下文中的我自己的单线程上下文,以尝试使asyncMVC子操作起作用而不必重复代码。但是,不仅会失去可伸缩性的好处(至少对于请求的那部分而言),而且假设ASP.NET API在ASP.NET上下文中运行,您还会遇到它们。
async
因此,我从未在生产中使用这种方法。我只是在必要时使用了同步API。