如何在 ASP.NET Web API 中使用非线程安全的 async/await API 和模式?

Posted

技术标签:

【中文标题】如何在 ASP.NET Web API 中使用非线程安全的 async/await API 和模式?【英文标题】:How to use non-thread-safe async/await APIs and patterns with ASP.NET Web API? 【发布时间】:2014-01-26 09:32:24 【问题描述】:

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

最初的问题是那里有很多有用的 .NET API(例如 Microsoft Entity Framework 的 DbContext),它们提供了设计用于 await 的异步方法,但它们被记录为 不是线程安全的。这使得它们非常适合在桌面 UI 应用程序中使用,但不适用于服务器端应用程序。 [EDITED] 这实际上可能不适用于DbContext,这是微软的statement on EF6 thread safety,请自行判断。 [/EDITED]

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

using (var docClient = CreateDocumentServiceClient())
using (new OperationContextScope(docClient.InnerChannel))

    return await docClient.GetDocumentAsync(docId);

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

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

我一直在考虑如何在不牺牲可伸缩性的情况下解决这个问题。显然,让这些 API 恢复的唯一方法是为范围保持线程亲和性的异步调用可能受线程切换影响。

假设我们有这样的线程亲和力。无论如何,这些调用中的大多数都是 IO 绑定的 (There Is No Thread)。当一个异步任务挂起时,它发起的线程可用于为另一个类似任务的延续提供服务,该结果已经可用。因此,它不应该过多地损害可扩展性。这种做法并不是什么新鲜事,其实类似的单线程模型是successfully used by Node.js。 IMO,这是让 Node.js 如此受欢迎的原因之一。

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

以下是来自the linked question 的非线程安全 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);

我继续使用 implemented ThreadAffinityTaskScheduler 作为概念证明,基于 Stephen Toub 的出色 StaTaskSchedulerThreadAffinityTaskScheduler 维护的池线程不是经典 COM 意义上的 STA 线程,但它们确实实现了 await 延续的线程关联(SingleThreadSynchronizationContext 负责)。

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

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

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

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

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

【问题讨论】:

据我了解,“非线程安全”一词并不意味着您不能从不同的线程访问对象,只是必须以某种方式同步来自不同线程的访问。我认为允许DbContext 的控制在线程之间移动没有问题,只要在任何时候只有一个线程可以访问它。您引用的对象类型是 STA 对象,它们的处理方式非常不同。 @Aron,就像我说的,没有并发 API 访问,但 API 仍然可能失败,例如因为它使用线程本地存储。我自己没有在 ASP.NET 中使用过DbContext/DataContext,但我相信链接的question 的 OP。 在this articleStephen Toub 中特别提到 async/await 设施正在流动 ExecutionContext,因此是 TLS,不是吗? @G.Stoynev,不,ExecutionContext 不会自动传输整个 TLS 状态,它应该如此。我已经解决了您的评论,并提供了更多详细信息here。 【参考方案1】:

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

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

2-3。我使用了直接嵌套在 ASP.NET 上下文中的my own single-threaded context,试图在不复制代码的情况下获得async MVC child actions working。但是,您不仅失去了可扩展性优势(至少对于请求的那部分),而且假设它们在 ASP.NET 上下文中运行,您还会遇到 ASP.NET API。

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

【讨论】:

“应该很好地处理线程跳转到等待点”是什么意思?据我了解(我可能是错的),如果 EF6,他们总是使用 ConfigureAwait(false) 进行 XXXAsync,所以执行的代码 after await 可能 不会在相同的情况下运行thread 最初进行异步调用。所以基本上上下文被放置在另一个线程上。这是预期的行为吗?文档没有说 DbContext 支持对实例成员的任何多线程调用。 @ken2k 如果 EF6 在内部使用 ConfigureAwait(false),那么 EF 代码中 await 之后的任何内容(通常)都不会在 ASP.NET 上下文中运行。但是,如果您在调用 EF 的代码中不使用 ConfigureAwait(false),那么您的 await 之后的代码将在 ASP.NET 上下文中运行。 @svick 有道理,感谢您的澄清。 @StephenCleary,关于线程的上下文属性(CultureCurrentPrincipalExecutionContext 等),这是一个很好的观点。我想如果需要我可以让它们流动起来。关于SynchronizationContext.Current is AspNetSynchronizationContext 检查,我认为将ThreadAffinityTaskScheduler.Run 调用限制为仅在真正需要的情况下进行,不包括AspNetSynchronizationContext-aware API。 @StephenCleary,关于 EF 线程跳转的声明是否适用于 EF 【参考方案2】:

您不应该将多线程与异步交织在一起。对象不是线程安全的问题是多个线程同时访问单个实例(或静态)。对于异步调用,可能在延续中从不同的线程访问上下文,但绝不会同时访问(当没有在多个请求之间共享时,但这首先是不好的)。

【讨论】:

查看该问题的第一条评论和我的回复。

以上是关于如何在 ASP.NET Web API 中使用非线程安全的 async/await API 和模式?的主要内容,如果未能解决你的问题,请参考以下文章