一尘不染

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

c#

此问题已由EF数据上下文-
异步/等待和多线程
触发。我已经回答了一个,但是还没有提供任何最终解决方案。

最初的问题是,那里有很多有用的.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请求。使用AspNetSynchronizationContextawait可以在与发起异步操作的线程不同的线程上排队一个延续,同时将原始线程释放到池中,并可以用于服务另一个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);
    }
}

// ...

// 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的出色表现,我继续进行并 作为概念证明进行了
实施ThreadAffinityTaskSchedulerStaTaskSchedulerThreadAffinityTaskScheduler在经典的COM意义上,由其维护的池线程不是STA线程,但是它们确实实现了线程的await连续性(SingleThreadSynchronizationContext对此负责)。

到目前为止,我已经将该代码作为控制台应用程序进行了测试,并且看起来可以按设计工作。我尚未在ASP.NET页面中对其进行测试。我没有大量的ASP.NET生产开发经验,所以我的问题是:

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

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

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

  4. 任何有关如何使用ASP.NET进行压力测试和概要分析的建议都将受到赞赏。


阅读 346

收藏
2020-05-19

共1个答案

一尘不染

实体框架将(应该)处理跨await点的线程跳转。如果没有,那是EF中的错误。OTOH
OperationContextScope基于TLS,并且await不安全。

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

2-3。我使用直接嵌套在ASP.NET上下文中的我自己的单线程上下文,以尝试使asyncMVC子操作起作用而不必重复代码。但是,不仅会失去可伸缩性的好处(至少对于请求的那部分而言),而且假设ASP.NET
API在ASP.NET上下文中运行,您还会遇到它们。

因此,我从未在生产中使用这种方法。我只是在必要时使用了同步API。

2020-05-19