如何在 ASP.NET Web API 中使用非线程安全的异步/等待 API 和模式? [英] How to use non-thread-safe async/await APIs and patterns with ASP.NET Web API?

查看:30
本文介绍了如何在 ASP.NET Web API 中使用非线程安全的异步/等待 API 和模式?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

这个问题是由 EF Data Context - Async/Await & 触发的多线程.我已经回答了那个,但没有提供任何最终解决方案.

This question has been triggered by EF Data Context - Async/Await & Multithreading. I've answered that one, but haven't provided any ultimate solution.

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

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. That makes them great for use in desktop UI apps, but not for server-side apps. This might not actually apply to DbContext, here is Microsoft's statement on EF6 thread safety, judge for yourself. [/EDITED]

还有一些已建立的代码模式属于同一类别,例如使用 OperationContextScope(询问这里这里),例如:

There are also some established code patterns falling into the same category, like calling a WCF service proxy with OperationContextScope (asked here and here), e.g.:

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

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

This may fail because OperationContextScope uses thread local storage in its implementation.

问题的根源在于AspNetSynchronizationContext,它在异步ASP.NET 页面中使用,以使用来自ASP.NET 线程池的更少线程来满足更多HTTP 请求.使用 AspNetSynchronizationContextawait 延续可以在与启动异步操作的线程不同的线程上排队,而原始线程被释放到池中,可用于服务另一个 HTTP 请求.这大大提高了服务器端代码的可扩展性.关于 SynchronizationContext,必读.因此,虽然不涉及并发 API 访问,但潜在的线程切换仍会阻止我们使用上述 API.

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. 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. 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. So, while there is no concurrent API access involved, a potential thread switch still prevents us from using the aforementioned APIs.

我一直在考虑如何在不牺牲可扩展性的情况下解决这个问题.显然,恢复这些 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.

假设我们有这样的线程关联.无论如何,这些调用中的大多数都是 IO 绑定的(没有线程).当异步任务挂起时,它所在的线程可用于服务另一个类似任务的延续,该任务的结果已经可用.因此,它不应该过多地损害可扩展性.这种方法并不新鲜,事实上,一个类似的单线程模型被Node.js 成功使用.IMO,这是使 Node.js 如此受欢迎的原因之一.

Let's say we have such thread affinity. Most of those calls are IO-bound anyway (There Is No Thread). 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. IMO, this is one of those things that make Node.js so popular.

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

I don't see why this approach could not be used in ASP.NET context. A custom task scheduler (let's call it ThreadAffinityTaskScheduler) might maintain a separate pool of "affinity apartment" threads, to improve scalability even further. 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.

以下是 链接问题 中的非线程安全 API 如何与此类 ThreadAffinityTaskScheduler<一起使用/代码>:

Here's how a non-thread-safe API from the linked question might be used with such 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 作为概念证明,基于 Stephen Toub 的优秀StaTaskScheduler.ThreadAffinityTaskScheduler 维护的池线程不是经典 COM 意义上的 STA 线程,但它们确实为 await 延续实现了线程关联(SingleThreadSynchronizationContext 负责为此).

I went ahead and implemented ThreadAffinityTaskScheduler as a proof of concept, based on the Stephen Toub's excellent 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).

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

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. I don't have a lot of production ASP.NET development experience, so my questions are:

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

  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)?

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

Is there alternative approaches, besides using synchronous API invocations or avoiding those APis at all?

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

Has anyone used something similar in ASP.NET MVC or Web API projects and is ready to share his/her experience?

关于如何使用 ASP.NET 对这种方法进行压力测试和分析的任何建议都是不胜感激.

Any advice on how to stress-test and profile this approach with ASP.NET would be appreciated.

推荐答案

Entity Framework 将(应该)处理跨 await 点的线程跳转就好了;如果没有,那么这是 EF 中的一个错误.OTOH,OperationContextScope 基于 TLS,不是 await 安全的.

Entity Framework will (should) handle thread jumps across await points just fine; if it doesn't, then that's a bug in EF. OTOH, OperationContextScope is based on TLS and is not await-safe.

1.同步 API 维护您的 ASP.NET 上下文;这包括在处理过程中通常很重要的用户身份和文化等内容.此外,许多 ASP.NET API 假设它们在实际的 ASP.NET 上下文中运行(我的意思不是只使用 HttpContext.Current;我的意思是实际上假设 SynchronizationContext.CurrentAspNetSynchronizationContext 的一个实例.

1. Synchronous APIs maintain your ASP.NET context; 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).

2-3.我使用了 我自己的单线程上下文 直接嵌套在 ASP.NET 上下文中,在尝试使 async MVC 子操作正常工作而无需复制代码.但是,您不仅失去了可扩展性优势(至少对于请求的那部分而言),而且假设它们在 ASP.NET 上下文中运行,您还会遇到 ASP.NET API.

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. 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.

所以,我从未在生产中使用过这种方法.我只是在必要时使用同步 API.

So, I have never used this approach in production. I just end up using the synchronous APIs when necessary.

这篇关于如何在 ASP.NET Web API 中使用非线程安全的异步/等待 API 和模式?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆