那里有很多有用的.NET API(例如Microsoft Entity Framework的API DbContext),它们提供了旨在与之一起使用的异步方法await,但据记录它们不是线程安全的。这使其非常适合在桌面UI应用程序中使用,但不适用于服务器端应用程序。[编辑] 这可能实际上并不适用DbContext,这是Microsoft 关于EF6线程安全性的声明,请自行判断。 [/编辑]
也有一些已建立的代码模式属于同一类别,例如使用OperationContextScope(在此处和此处询问)调用WCF服务代理,例如:
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。
我一直在考虑如何在不牺牲可伸缩性的情况下解决此问题。显然,收回这些API的唯一方法是针对可能受到线程切换影响的异步调用范围保持线程亲和性。
假设我们具有这种线程相似性。无论如何,这些调用中的大多数都是受IO约束的(没有线程)。当异步任务挂起时,它所源自的线程可用于服务另一个类似任务的延续,该结果已经可用。因此,它不应过多地损害可伸缩性。这种方法并不是什么新鲜事物,实际上,Node.js已成功使用了类似的单线程模型。IMO,这是使Node.js如此受欢迎的原因之一。
我不明白为什么这种方法不能在ASP.NET上下文中使用。定制任务调度程序(我们称之为ThreadAffinityTaskScheduler)可以维护一个单独的“亲和公寓”线程池,以进一步提高可伸缩性。一旦任务已排队到那些“公寓”线程之一中,await任务内部的所有继续将在同一线程上进行。
这是链接问题中的非线程安全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);
}
}
// ...
基于Stephen Toub的出色表现,我继续进行并作为概念证明进行了实施ThreadAffinityTaskSchedulerStaTaskScheduler。ThreadAffinityTaskScheduler在经典的COM意义上,由其维护的池线程不是STA线程,但是它们确实实现了线程的await连续性(SingleThreadSynchronizationContext对此负责)。
到目前为止,我已经将该代码作为控制台应用程序进行了测试,并且看起来可以按设计工作。我尚未在ASP.NET页面中对其进行测试。我没有大量的ASP.NET生产开发经验,所以我的问题是:
在ASP.NET中非线程安全的API的简单同步调用上使用这种方法是否有意义(主要目标是避免牺牲可伸缩性)?
除了使用同步API调用或完全避免这些APis之外,还有其他方法吗?
有没有人在ASP.NET MVC或Web API项目中使用过类似的东西,并准备分享他/她的经验?
任何有关如何使用ASP.NET进行压力测试和概要分析的建议都将受到赞赏。
白衣染霜花