简体   繁体   English

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

[英]How to use non-thread-safe async/await APIs and patterns with ASP.NET Web API?

This question has been triggered by EF Data Context - Async/Await & Multithreading . 此问题已由EF数据上下文 - 异步/等待和多线程触发。 I've answered that one, but haven't provided any ultimate solution. 我已经回答了那一个,但没有提供任何最终的解决方案。

The original problem is that there are a lot of useful .NET APIs out there (like Microsoft Entity Framework's DbContext ), which provide asynchronous methods designed to be used with await , yet they are documented as not thread-safe . 最初的问题是有许多有用的.NET API(如Microsoft Entity Framework的DbContext ),它提供了设计用于await异步方法,但它们被记录为不是线程安全的 That makes them great for use in desktop UI apps, but not for server-side apps. 这使它们非常适合在桌面UI应用程序中使用,但不适用于服务器端应用程序。 [EDITED] This might not actually apply to DbContext , here is Microsoft's statement on EF6 thread safety , judge for yourself. [编辑] 这可能实际上并不适用于DbContext ,这是微软关于EF6线程安全声明 ,自己判断。 [/EDITED] [/ EDITED]

There are also some established code patterns falling into the same category, like calling a WCF service proxy with OperationContextScope (asked here and here ), eg: 还有一些已建立的代码模式属于同一类别,例如使用OperationContextScope调用WCF服务代理( 此处此处询问),例如:

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

This may fail because OperationContextScope uses thread local storage in its implementation. 这可能会失败,因为OperationContextScope在其实现中使用线程本地存储。

The source of the problem is AspNetSynchronizationContext which is used in asynchronous ASP.NET pages to fulfill more HTTP requests with less threads from ASP.NET thread pool. 问题的根源是AspNetSynchronizationContext ,它在异步ASP.NET页面中用于使用来自ASP.NET线程池的较少线程来完成更多HTTP请求。 With AspNetSynchronizationContext , an await continuation can be queued on a different thread from the one which initiated the async operation, while the original thread is released to the pool and can be used to serve another HTTP request. 使用AspNetSynchronizationContextawait continuation可以在与启动异步操作的线程不同的线程上排队,而原始线程将释放到池中,并可用于提供另一个HTTP请求。 This substantially improves the server-side code scalability. 这大大提高了服务器端代码的可扩展性。 The mechanism is described in great details in It's All About the SynchronizationContext , a must-read. 该机制在It's All About the SynchronizationContext中有详细描述,必须阅读。 So, while there is no concurrent API access involved, a potential thread switch still prevents us from using the aforementioned APIs. 因此,虽然没有涉及并发API访问 ,但潜在的线程切换仍然阻止我们使用上述API。

I've been thinking about how to solve this without sacrificing the scalability. 我一直在考虑如何在不牺牲可扩展性的情况下解决这个问题。 Apparently, the only way to have those APIs back is to maintain thread affinity for the scope of the async calls potentially affected by a thread switch. 显然,恢复这些API的唯一方法是维护可能受线程切换影响的异步调用范围的线程关联

Let's say we have such thread affinity. 假设我们有这样的线程亲和力。 Most of those calls are IO-bound anyway ( There Is No Thread ). 这些调用中的大多数都是IO绑定的( 没有线程 )。 While an async task is pending, the thread it's been originated on can be used to serve a continuation of another similar task, which result is already available. 当异步任务处于挂起状态时,它所源自的线程可用于提供另一个类似任务的延续,该结果已经可用。 Thus, it shouldn't hurt scalability too much. 因此,它不应该过多地损害可伸缩性。 This approach is nothing new, in fact, a similar single-threaded model is successfully used by Node.js . 这种方法并不新鲜,事实上, Node.js成功使用了类似的单线程模型 IMO, this is one of those things that make Node.js so popular. IMO,这是使Node.js如此受欢迎的事情之一。

I don't see why this approach could not be used in ASP.NET context. 我不明白为什么这种方法不能在ASP.NET上下文中使用。 A custom task scheduler (let's call it ThreadAffinityTaskScheduler ) might maintain a separate pool of "affinity apartment" threads, to improve scalability even further. 自定义任务调度程序(我们称之为ThreadAffinityTaskScheduler )可能会维护一个单独的“亲和性单元”线程池,以进一步提高可伸缩性。 Once the task has been queued to one of those "apartment" threads, all await continuations inside the task will be taking place on the very same thread. 一旦任务排队到其中一个“公寓”线程,所有await任务内部的延续都将在同一个线程上进行。

Here's how a non-thread-safe API from the linked question might be used with such ThreadAffinityTaskScheduler : 以下是链接问题中的非线程安全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);

I went ahead and implemented ThreadAffinityTaskScheduler as a proof of concept , based on the Stephen Toub's excellent StaTaskScheduler . 基于Stephen Toub优秀的StaTaskScheduler ,我继续实施ThreadAffinityTaskScheduler作为概念 StaTaskScheduler The pool threads maintained by ThreadAffinityTaskScheduler are not STA thread in the classic COM sense, but they do implement thread affinity for await continuations ( SingleThreadSynchronizationContext is responsible for that). ThreadAffinityTaskScheduler维护的池线程在经典COM意义上不是STA线程,但它们确实实现了线程关联以await延续( SingleThreadSynchronizationContext负责)。

So far, I've tested this code as console app and it appears to work as designed. 到目前为止,我已将此代码测试为控制台应用程序,它似乎按设计工作。 I haven't tested it inside an ASP.NET page yet. 我还没有在ASP.NET页面中测试它。 I don't have a lot of production ASP.NET development experience, so my questions are: 我没有很多生产ASP.NET开发经验,所以我的问题是:

  1. Does it make sense to use this approach over simple synchronous invocation of non-thread-safe APIs in ASP.NET (the main goal is to avoid sacrificing scalability)? 使用这种方法而不是简单地同步调用ASP.NET中的非线程安全API(主要目标是避免牺牲可伸缩性)是否有意义?

  2. Is there alternative approaches, besides using synchronous API invocations or avoiding those APis at all? 是否有其他方法,除了使用同步API调用或完全避免这些AP?

  3. Has anyone used something similar in ASP.NET MVC or Web API projects and is ready to share his/her experience? 有没有人在ASP.NET MVC或Web API项目中使用类似的东西,并准备分享他/她的经验?

  4. Any advice on how to stress-test and profile this approach with ASP.NET would be appreciated. 关于如何使用ASP.NET进行压力测试和分析这种方法的任何建议都将受到赞赏。

Entity Framework will (should) handle thread jumps across await points just fine; 实体框架将(应该)处理await点的线程跳转就好了; if it doesn't, then that's a bug in EF. 如果没有,那么这就是EF中的一个错误。 OTOH, OperationContextScope is based on TLS and is not await -safe. OTOH, OperationContextScope基于TLS,不await -safe。

1. Synchronous APIs maintain your ASP.NET context; 1.同步API维护您的ASP.NET上下文; this includes things such as user identity and culture that are often important during processing. 这包括在处理过程中通常很重要的用户身份和文化等内容。 Also, a number of ASP.NET APIs assume they are running on an actual ASP.NET context (I don't mean just using HttpContext.Current ; I mean actually assuming that SynchronizationContext.Current is an instance of AspNetSynchronizationContext ). 此外,许多ASP.NET API假设它们在实际的ASP.NET上下文中运行(我并不仅仅意味着使用HttpContext.Current ;我的意思是实际上假设SynchronizationContext.CurrentAspNetSynchronizationContext一个实例)。

2-3. 2-3。 I have used my own single-threaded context nested directly within the ASP.NET context, in attempts to get async MVC child actions working without having to duplicate code. 我使用自己的单线程上下文直接嵌套在ASP.NET上下文中,试图让async MVC子操作工作而不必重复代码。 However, not only do you lose the scalability benefits (for that part of the request, at least), you also run into the ASP.NET APIs assuming that they're running on an ASP.NET context. 但是,您不仅会失去可伸缩性优势(至少对于该部分请求而言),您还会遇到ASP.NET API,假设它们在ASP.NET上下文中运行。

So, I have never used this approach in production. 所以,我从未在生产中使用过这种方法。 I just end up using the synchronous APIs when necessary. 我只是在必要时最终使用同步API。

You should not intertwine multithreading with asynchrony. 您不应该将多线程与异步交织在一起。 The problem with an object not being thread-safe is when a single instance (or static) is accessed by multiple threads at the same time . 对象不是线程安全的问题是多个线程同时访问单个实例(或静态) With async calls the context is possibly accessed from a different thread in the continuation, but never at the same time (when not shared across multiple requests, but that isn't good in the first place). 随着异步调用上下文可能在延续一个不同的线程访问,但从来没有在同一时间(当在多个请求未共享,但不是好摆在首位)。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM