如何使用ASP.NET Web API使用非线程安全的asynchronous/等待API和模式?

这个问题已经被EF数据上下文 – asynchronous/等待和multithreading触发。 我已经回答了一个,但没有提供任何最终解决scheme。

原来的问题是有很多有用的.NET API(比如Microsoft Entity Framework的DbContext ),它提供了asynchronous方法,用于await ,但它们被logging为不是线程安全的 。 这使得它们非常适合在桌面UI应用程序中使用,但不适用于服务器端应用程序。 [EDITED] 这实际上可能不适用于DbContext ,这里是微软对DbContext 线程安全性的陈述 ,请DbContext判断。 [/ EDITED]

还有一些已经build立的代码模式属于同一类别,比如使用OperationContextScope调用WCF服务代理( 在这里和这里问),例如:

 using (var docClient = CreateDocumentServiceClient()) using (new OperationContextScope(docClient.InnerChannel)) { return await docClient.GetDocumentAsync(docId); } 

这可能会失败,因为OperationContextScope在其实现中使用线程本地存储。

问题的根源是AspNetSynchronizationContext ,用于asynchronousASP.NET页面,以便从ASP.NET线程池执行更多的线程数更less的HTTP请求。 使用AspNetSynchronizationContext ,可以将await继续排队在与启动asynchronous操作的线程不同的线程上,同时将原始线程释放到池中,并可用于服务另一个HTTP请求。 这大大提高了服务器端代码的可伸缩性。 该机制在“所有关于同步上下文”中有详细描述, 这是一个必读内容。 所以,虽然没有涉及到并发的API访问 ,但潜在的线程切换仍然阻止我们使用上述的API。

我一直在考虑如何解决这个问题,而不牺牲可扩展性。 显然,这些API返回的唯一方法是维护线程切换可能影响的asynchronous调用范围的线程关系

比方说,我们有这样的线程亲和力。 这些调用大部分都是IO绑定( 没有线程 )。 asynchronous任务处于待处理状态时,可以使用它发起的线程来继续执行另一个类似的任务,该结果已经可用。 因此,它不应该伤害可扩展性太多。 这种方法并不新鲜,事实上, Node.js成功地使用了一个类似的单线程模型 。 海事组织,这是使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); } } // ... // 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); 

我继续实施ThreadAffinityTaskScheduler作为概念certificate ,基于Stephen Toub的优秀StaTaskScheduler 。 由ThreadAffinityTaskScheduler维护的池线程不是经典COM意义上的STA线程,但是它们为await延续实现了线程关联( SingleThreadSynchronizationContext负责这一点)。

到目前为止,我已经testing了这个代码作为控制台应用程序,它似乎按devise工作。 我还没有在ASP.NET页面中testing它。 我没有很多生产ASP.NET的开发经验,所以我的问题是:

  1. 在ASP.NET中使用非线程安全API的简单同步调用(主要目标是避免牺牲可伸缩性)使用这种方法是否合理?

  2. 除了使用同步API调用或者完全避免这些AP之外,还有其他方法吗?

  3. 有没有人在ASP.NET MVC或Web API项目中使用类似的东西,并准备分享他/她的经验?

  4. 任何build议如何压力testing和分析这种方法与ASP.NET将不胜感激。

entity framework将(应)处理线程跳转await点很好; 如果没有,那么这是EF中的一个错误。 OTOH, OperationContextScope基于TLS,不await

1.同步API维护您的ASP.NET上下文; 这包括在处理过程中经常重要的诸如用户身份和文化等事物。 另外,许多ASP.NET API假定它们在实际的ASP.NET上下文中运行(我不是说只使用HttpContext.Current ;实际上假设SynchronizationContext.CurrentAspNetSynchronizationContext一个实例)。

2-3。 我已经使用自己的单线程上下文直接嵌套在ASP.NET上下文中,试图获得async MVC子操作而不必重复代码。 但是,您不仅失去了可伸缩性的好处(至less对于这部分请求),还可以运行在ASP.NET API上,假设它们在ASP.NET上下文中运行。

所以,我从来没有在生产中使用过这种方法。 我只是在必要时才使用同步API。

你不应该把asynchronousmultithreading交织在一起。 对象不是线程安全的问题是多个线程同时访问单个实例(或静态)。 对于asynchronous调用,上下文可能是从不同的线程继续进行访问的,但从来不会同时进行(当多个请求不共享时,首先就不好)。