这个问题是由EF Data Context - Async/Await & Multithreading个问题引发的.我已经回答了这个问题,但还没有提供任何最终的解决方案.
最初的问题是有很多有用的.NETAPI(比如Microsoft Entity Framework的DbContext
),它们提供了设计用于await
的异步方法,但是它们的文档记录为not thread-safe.这使得它们非常适合在桌面UI应用程序中使用,但不适用于服务器端应用程序.[EDITED] This might not actually apply to 103, here is Microsoft's 101, judge for yourself. [/EDITED]
还有一些已建立的代码模式属于同一类别,比如用OperationContextScope
(询问here和here)调用WCF服务代理,例如:
using (var docClient = CreateDocumentServiceClient())
using (new OperationContextScope(docClient.InnerChannel))
{
return await docClient.GetDocumentAsync(docId);
}
这可能会失败,因为OperationContextScope
在其实现中使用了线程本地存储.
问题的根源是异步ASP中使用的AspNetSynchronizationContext
.NET页面,用ASP.NET
线程池中较少的线程来完成更多HTTP请求.对于AspNetSynchronizationContext
,await
延续可以在发起异步操作的线程的不同线程上排队,而原始线程被释放到池中,并可用于服务另一个HTTP请求.This substantially improves the server-side code scalability.《必读》It's All About the SynchronizationContext中详细介绍了该机制.因此,尽管涉及there is no concurrent API access,但潜在的线程切换仍然阻止我们使用上述API.
I've been thinking about how to solve this without sacrificing the scalability.显然,对于可能受线程切换影响的异步调用的范围,将这些API恢复到maintain thread affinity是唯一的方法.
让我们假设我们有这样的线程亲和力.这些调用中的大多数无论如何都是IO绑定的(There Is No Thread).当异步任务处于挂起状态时,发起该任务的线程可以用来为另一个类似任务的后续任务提供服务,该任务的结果已经可用.因此,它应该不会对可伸缩性造成太大影响.这种做法并不是什么 fresh 事,实际上是a similar single-threaded model is 101.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);
我以Stephen Toub的优秀StaTaskScheduler
为基础,获得了100 as a proof of concept分.ThreadAffinityTaskScheduler
维护的池线程不是典型COM意义上的STA线程,但它们确实实现了await
个延续的线程关联(SingleThreadSynchronizationContext
负责).
到目前为止,我已经将这段代码作为控制台应用程序进行了测试,它似乎按照设计工作.我还没有在ASP中测试过它.网页还没有.我没有很多生产ASP.NET开发经验,所以我的问题是:
在ASP.NET中对非线程安全API的简单synchronous调用使用此方法有意义吗(主要目标是避免牺牲可伸缩性)?
除了使用同步API调用或完全避免这些API之外,还有其他方法吗?
有人在ASP中使用过类似的东西吗.NET MVC或Web API项目,并准备好分享他/她的经验?
关于如何使用ASP对这种方法进行压力测试和分析的任何建议.净是