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

nos*_*tio 33 .net c# asp.net task-parallel-library async-await

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

最初的问题是,有许多有用的.NET API(如Microsoft Entity Framework DbContext),它们提供了设计用于的异步方法await,但它们被记录为不是线程安全的.这使它们非常适合在桌面UI应用程序中使用,但不适用于服务器端应用程序.[编辑] 这可能实际上并不适用DbContext,这是微软关于EF6线程安全声明,自己判断. [/ EDITED]

还有一些已建立的代码模式属于同一类别,例如调用WCF服务代理OperationContextScope(在此处此处询问),例如:

using (var docClient = CreateDocumentServiceClient())
using (new OperationContextScope(docClient.InnerChannel))
{
    return await docClient.GetDocumentAsync(docId);
}
Run Code Online (Sandbox Code Playgroud)

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

问题的根源AspNetSynchronizationContext是在异步ASP.NET页面中使用,以便从ASP.NET线程池中使用更少的线程来满足更多HTTP请求.使用时AspNetSynchronizationContext,await可以在与启动异步操作的线程不同的线程上对延续进行排队,同时将原始线程释放到池中,并可用于提供另一个HTTP请求.这大大提高了服务器端代码的可扩展性.该机制在It's All About the SynchronizationContext中有详细描述,必须阅读.因此,虽然没有涉及并发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);
Run Code Online (Sandbox Code Playgroud)

基于Stephen Toub的出色表现,我开始实施ThreadAffinityTaskScheduler概念验证StaTaskScheduler.维护的池线程ThreadAffinityTaskScheduler不是经典COM意义上的STA线程,但它们确实为awaitcontinuation 实现线程关联(SingleThreadSynchronizationContext负责).

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

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

  2. 是否有其他方法,除了使用同步API调用或完全避免这些AP?

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

  4. 有关如何使用ASP.NET进行压力测试和分析此方法的任何建议将不胜感激.

Ste*_*ary 9

实体框架将(应该)处理跨await点的线程跳转就好了; 如果没有,那么这就是EF中的一个错误.OTOH,OperationContextScope基于TLS而不是await-safe.

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.

  • @ ken2k如果EF6在内部使用`ConfigureAwait(false)`,那么EF代码(通常)中的`await`之后的任何内容都不会在ASP.NET上下文中运行.但是如果你不在调用EF的代码中使用`ConfigureAwait(false)`,那么`await`之后的代码将在ASP.NET上下文中运行. (3认同)