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

18 浏览
0 Comments

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

这个问题是由于EF数据上下文-异步/等待和多线程而引发的。我回答了那个问题,但没有提供任何最终解决方案。

原始问题是有很多有用的.NET API(如Microsoft Entity Framework的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页面中使用,以使用较少的线程来满足更多的HTTP请求来自ASP.NET线程池。使用AspNetSynchronizationContextawait继续可以在与启动异步操作的线程不同的线程上排队,同时原始线程被释放到池中,并且可以用于服务另一个HTTP请求。这极大地提高了服务器端代码的可扩展性。该机制在It's All About the SynchronizationContext中有详细描述,必读。因此,虽然没有并发的API访问,但潜在的线程切换仍然阻止我们使用上述API。

我一直在思考如何在不牺牲可扩展性的情况下解决这个问题。显然,要恢复这些API的唯一方法是在可能受到线程切换影响的异步调用范围内保持线程亲和性

假设我们有这样的线程亲和性。这些调用中的大多数都是IO绑定的(There Is No Thread)。在异步任务挂起时,它所起源的线程可以用于服务另一个已经可用结果的类似任务的继续。因此,它不应该对可扩展性产生太大影响。这种方法并不新鲜,实际上,Node.js就使用了类似的单线程模型。在我看来,这就是使Node.js如此受欢迎的一些原因之一。

我不明白为什么这种方法不能在ASP.NET上下文中使用。一个自定义的任务调度程序(我们称之为ThreadAffinityTaskScheduler)可以维护一个单独的“亲和公寓”线程池,以进一步提高可扩展性。一旦任务被排队到其中一个“公寓”线程上,所有await继续都将在同一个线程上进行。

下面是如何使用这样的ThreadAffinityTaskScheduler来使用来自链接问题的非线程安全API的示例代码:

// 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作为概念验证,基于Stephen Toub出色的StaTaskScheduler。由ThreadAffinityTaskScheduler维护的线程池线程不是经典COM意义上的STA线程,但它们确实对await继续实现了线程亲和性(SingleThreadSynchronizationContext负责这一点)。

到目前为止,我已经将此代码作为控制台应用程序进行了测试,并且似乎按预期工作。我还没有在ASP.NET页面中对其进行测试。我没有很多生产ASP.NET开发经验,所以我的问题是:

  1. 在ASP.NET中使用这种方法而不是简单的同步调用非线程安全的API是否有意义(主要目标是避免牺牲可扩展性)?
  2. 是否有替代方法,除了使用同步API调用或完全避免使用这些API?
  3. 有人在ASP.NET MVC或Web API项目中使用过类似的东西,并准备分享他/她的经验吗?
  4. 如何对这种在ASP.NET中使用的方法进行压力测试和性能分析的建议将不胜感激。
0