当前位置: 首页 > 知识库问答 >
问题:

如何将非线程安全的Async/Await API和模式与ASP.NET Web API一起使用?

归俊
2023-03-14

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

最初的问题是,存在大量有用的.NET API(如Microsoft Entity Framework的dbcontext),它们提供了设计用于await的异步方法,但它们被记录为不是线程安全的。这使得它们很适合用于桌面UI应用程序,但不适用于服务器端应用程序。[编辑]这可能并不适用于dbcontext,下面是微软关于EF6线程安全的声明,请自行判断。[/编辑]

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

问题的根源是ASPNetSynchronizationContext,它在异步ASP.NET页中使用,以使用ASP.NET线程池中较少的线程来实现较多的HTTP请求。使用ASPNetSynchronizationContextAwait延续可以在与启动异步操作的线程不同的线程上排队,而原始线程被释放到池中,并可以用于服务另一个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);

我在Stephen Toub出色的StataskScheduler的基础上实现了ThreadAffinityTaskScheduler作为概念证明。由ThreadAffinityTaskScheduler维护的池线程不是传统COM意义上的STA线程,但它们确实实现了Await延续的线程关联(SinglethreadSynchronizationContext负责此操作)。

到目前为止,我已经测试了这段代码作为控制台应用程序,它似乎可以按照设计工作。我还没有在ASP.NET页面中测试它。我没有大量的生产ASP.NET开发经验,因此我的问题是:

>

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

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

    如果有任何关于如何用ASP.NET对这种方法进行压力测试和分析的建议,将不胜感激。

  • 共有1个答案

    郭麒
    2023-03-14

    实体框架将(应该)很好地处理跨await点的线程跳转;如果不是,那就是EF的一个bug。此外,OperationContextScope基于TLS,不是Await安全的。

    1.同步API维护您的ASP.NET上下文;这包括在处理过程中经常很重要的诸如用户身份和区域性之类的东西。此外,许多ASP.NET API假设它们运行在实际的ASP.NET上下文上(我不是说仅仅使用HttpContext.Current;我是说实际上假设SynchronizationContext.CurrentASPNetSynchronizationContext的一个实例)。

    2-3。我使用了直接嵌套在ASP.NET上下文中的自己的单线程上下文,试图使asyncMVC子操作工作而不必重复代码。但是,您不仅失去了可伸缩性好处(至少对于请求的这一部分),而且还遇到了ASP.NET API(假设它们运行在ASP.NET上下文上)。

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

     类似资料:
    • 在Martin Fowler的书中,我读到了和模式。 作者提到,将identityMap放在UnitOfWork内部是一个好主意。但怎么做呢? 据我所知,受会话限制,但作者没有提到 每个unitOfWork实例需要多少个IdentityMap实例? 如果我们有两个并发请求呢?

    • 我尝试使ajv使用两个JSON-Schema,一个依赖于另一个。下面是我的模式的一个示例(简化): json 错误:没有带有键或引用“http://json-schema.org/draft-04/schema#”的模式 更新:如果我从types.json中删除“$schema…”,我得到的错误是: MissingReferror:无法从id#中解析引用types.json#/definition

    • 我有一个第三方图书馆提供一个类。他们的文件说: 我做了一些测试,创建了100个线程。实际上,如果我将同一个对象传递给所有100个线程,就会出现线程安全问题,但如果每个线程都创建自己的类实例,这些问题就会消失。 我在这里使用的是.NET4.0。在我的应用程序中,有多个线程想要使用这个类,但是我不想创建和销毁超过必要的对象(它们应该在应用程序的生存期内存在),所以我希望每个线程都有一个。这是否适合使用

    • 我看到了不同的PHP二进制文件,比如非线程或线程安全? 这是什么意思? 这些软件包之间有什么区别?

    • 即;每个可调用方调用progressBarUpdate(): 每个doSomeStuff()都有自己的异常处理,如果发生错误或抛出异常,则返回一个空值。这就是为什么返回类型是List,并且在这种情况下返回null的原因。调用项和它们返回的文件列表之间没有交叉,它们都维护自己的文件列表。 我发现它工作得很好,但偶尔会抛出窗体的InterruptedException: 我修改了代码,使条件nv>=m

    • 本文向大家介绍如何理解Java中的StringBuffer是线程安全的而StringBuilder是非线程安全的?,包括了如何理解Java中的StringBuffer是线程安全的而StringBuilder是非线程安全的?的使用技巧和注意事项,需要的朋友参考一下 StringBuffer(线程安全) StringBuffer是线程安全的,这意味着它们具有同步方法来控制访问,因此一次只有一个线程可以