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

2023-09-02 10:18:07 作者:没错!我就是那么帅!

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

原来的问题是,有很多有用的.NET API的有(像微软实体框架的DbContext),它提供了设计用于异步方法与伺机,但他们被记录为不是线程安全的。这使得它们非常适合于桌面UI使用应用程序,而不是服务器端的应用程序。 这可能不是真正适用的DbContext ,这里是微软的statement在EF6线程安全,自己判断。的 [/ EDITED]

也有一些既定code模式落入同一类别,比如调用与WCF服务代理OperationContextScope (问这里和这里),例如:

使用(VAR docClient = CreateDocumentServiceClient()) 使用(新OperationContextScope(docClient.InnerChannel)) {     返回等待docClient.GetDocumentAsync(的docId); }

这可能失败,因为 OperationContextScope 将使用其实现线程本地存储。

这个问题的根源是 AspNetSynchronizationContext 这是用来在异步ASP.NET页面来实现从更多的HTTP请求用更少的线程ASP.NET 线程池。随着 AspNetSynchronizationContext ,一个等待续可排队从它启动的异步操作的一个不同的线程,而原始线程被释放到池中,并且可以用来为另一个HTTP请求。 这大大提高了服务器端的code的可扩展性。的机制在很大的细节的这是所有关于的SynchronizationContext ,不可不看。因此,尽管没有并发API访问参与进来,一个潜在的线程切换仍然prevents我们从使用上述的API。

我一直在思考如何解决这个不牺牲扩展性。显然,有这些API回来的唯一办法是为保持线程关联的范围的异步调用可能受到线程切换。

让我们说我们有这样的线程关联。大多数这些电话都是IO的限制呢(没有线程)。而一个异步任务未决,它已经起源于该线程可用于服务的延续另一个类似的任务,这导致已经可用。因此,它不应该伤害的可扩展性太大。这种方法是什么新鲜事了,事实上,类似的单线程模型成功地使用Node.js的 。国际海事组织,这是那些东西,让Node.js的如此受欢迎的。

我不明白为什么在ASP.NET背景下,这个方法不能使用。自定义任务调度器(姑且称之为 ThreadAffinityTaskScheduler )可能会维持一个单独的亲和力公寓线程池,以进一步提高可伸缩性。一旦任务已经排队到这些公寓的一个线程,所有的等待里面的任务的延续将发生在非常相同的线程。

非线程安全与线程同步技术

下面是如何从链接的问题可能与这种 ThreadAffinityTaskScheduler :

//创建ThreadAffinityTaskScheduler的全局实例 - 每个Web应用程序 公共静态类GlobalState {     公共静态ThreadAffinityTaskScheduler TaScheduler {获得;私定; }     公共静态GlobalState     {         GlobalState.TaScheduler =新ThreadAffinityTaskScheduler(             numberOfThreads:10);     } } // ... //运行它采用非线程安全的API任务 VAR的结果=等待GlobalState.TaScheduler.Run(()=> {     使用(VAR的DataContext =新的DataContext())     {         VAR东西=等待dataContext.someEntities.FirstOrDefaultAsync(E => e.Id == 1);         VAR morething =等待dataContext.someEntities.FirstOrDefaultAsync(E => e.Id == 2);         // ...         //变换东西和morething成线程安全的对象,并返回结果         返回的数据;     } },CancellationToken.None);

我继续和 实施 ThreadAffinityTaskScheduler 作为一种概念证明的基础上,斯蒂芬Toub出色的StaTaskScheduler.通过 ThreadAffinityTaskScheduler 维护的线程池是不是在传统的COM感STA线程,但这两个国家执行的线程关联等待延续( SingleThreadSynchronizationContext 负责的)。

到目前为止,我测试过这个code作为控制台应用程序,它似乎工作作为设计的。我没有测试过里面的ASP.NET页呢。我没有大量的生产ASP.NET开发经验,所以我的问题是:

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

有没有其他办法,除了使用同步API调用或避免这些API呢?

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

如何压力测试和配置文件这一做法与ASP.NET将任何意见 AP preciated。

解决方案

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

1。同步的API保持你的ASP.NET环境;这包括的东西,如用户身份和文化,往往在加工过程中很重要的。此外,一些ASP.NET的API假定它们是在实际的ASP.NET环境中运行(我的意思并不是只使用 HttpContext.Current ;我的意思是真正假设 SynchronizationContext.Current AspNetSynchronizationContext )。

实例

2-3。我已经使用直接嵌套在ASP.NET框架内,在尝试我自己的单线程上下文获取 异步工作无需复制MVC儿童行动code。但是,你不仅失去了可扩展性优势(该请求的一部分,至少),你也碰上了ASP.NET的API假定他们是在一个ASP.NET环境中运行。

所以,我从来没有在生产中使用这种方法。我刚刚结束了使用同步API的必要时。

This question has been triggered by EF Data Context - Async/Await & Multithreading. 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. That makes them great for use in desktop UI apps, but not for server-side apps. [EDITED] This might not actually apply to DbContext, here is Microsoft's statement on EF6 thread safety, judge for yourself. [/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), e.g.:

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.

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.

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.

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.

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.

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

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

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:

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

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

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

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

解决方案

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

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