【问题标题】:Parallelizing synchronous tasks while retaining the HttpContext.Current in ASP.NET在 ASP.NET 中保留 HttpContext.Current 的同时并行化同步任务
【发布时间】:2021-02-15 15:17:36
【问题描述】:

我已经在 SO 中搜索了答案,但没有找到与手头问题相关的答案,尽管 this one 指出了“为什么”,但没有解决它。

我有一个 REST 端点,它需要从其他端点收集数据 - 在这样做时,它会访问 HttpContext(设置身份验证、标头等……所有这些都是通过我无法访问的 3rd 方库完成的)。

不幸的是,这个用于服务通信的库是同步的,我们希望并行化它的使用。

在下面的示例(抽象)代码中,问题是 CallEndpointSynchronously 不幸地使用了一些内置身份验证,当 HttpContext 未设置时会引发空异常:

public class MyController: ApiController
//...

[HttpPost]
public async Task<IHttpActionResult> DoIt(IEnumerable<int> inputs)
{
    var tasks = inputs.Select(i => 
              Task.Run(()=> 
                 {
                    /* call some REST endpoints, pass some arguments, get the response from each.
                    The obvious answer (HttpContext.Current = parentContext) can't work because 
                    there's some async code underneath (for whatever reasons), and that would cause it 
                    to sometimes not return to the same thread, and basically abandon the Context, 
                    again resulting in null */

                    var results = Some3rdPartyTool.CallEndpointSynchronously(MyRestEndpointConfig[i]);
                    return results;
                 });
    var outcome = await Task.WhenAll(tasks);
    // collect outcome, do something with it, render outputs... 
}

有没有办法解决这个问题?
我们希望针对单个请求进行优化,目前对最大化并行用户不感兴趣。

【问题讨论】:

  • 如果我理解this post中的Stephen Cleary,那基本上是做不到的……对吧?
  • 它是什么框架? .NET 4、4.5?
  • 好吧,如果它是 4.5 或更高版本,您可以尝试使用它 - bartwullems.blogspot.com/2013/09/… 不确定它是否适用于这种特定情况
  • @NikitaChayka:现在是 4.7.2
  • 所以试试我提供的,可能会有所帮助,但再次 - 不确定

标签: c# task


【解决方案1】:

不幸的是,这个用于服务通信的库是同步的,我们希望并行化它的使用。

未设置 HttpContext 时抛出 null 异常:

显而易见的答案 (HttpContext.Current = parentContext) 不起作用,因为下面有一些异步代码(无论出于何种原因),这会导致它有时不会返回同一个线程,并且基本上再次放弃上下文结果为空

示例代码注释中有您问题的重要部分。 :)

通常,HttpContext 不应跨线程共享。它根本不是线程安全的。但是你可以设置HttpContext.Current(出于某种原因),所以你可以选择危险地生活。

这里更隐蔽的问题是该库有一个同步 API并且 正在执行异步同步 - 但不知何故没有死锁 (?)。在这一点上,我必须诚实地说,最好的方法是修复库:让供应商修复它,或者提交 PR,或者在必要时重写它。

但是,您可以通过添加更危险的代码来使这种工作正常进行。

所以,以下是您需要了解的信息:

  • ASP.NET (pre-Core) 使用AspNetSynchronizationContext。这个上下文:
    • 确保一次只有一个线程在此上下文中运行。
    • 为上下文中运行的任何线程设置HttpContext.Current

现在,您可以捕获SynchronizationContext.Current 并将其安装在线程池线程上,但除了非常危险之外,它不会实现您的实际目标(并行化),因为@ 987654327@ 一次只允许一个线程进入。第 3 方代码的第一部分将能够并行运行,但任何排队到 AspNetSynchronizationContext 的东西都会一次运行一个线程。

所以,我能想到的唯一方法是使用您自己的自定义SynchronizationContext,在同一线程上恢复,并在该线程上设置HttpContext.Current。我有一个AsyncContext class 可以用于此目的:

[HttpPost]
public async Task<IHttpActionResult> DoIt(IEnumerable<int> inputs)
{
  var context = HttpContext.Current;
  var tasks = inputs.Select(i =>
      Task.Run(() =>
          AsyncContext.Run(() =>
          {
            HttpContext.Current = context;
            var results = Some3rdPartyTool.CallEndpointSynchronously(MyRestEndpointConfig[i]);
            return results;
          })));
  var outcome = await Task.WhenAll(tasks);
}

所以对于每个输入,从线程池中抓取一个线程(Task.Run),安装自定义的单线程同步上下文(AsyncContext.Run),设置HttpContext.Current,然后有问题的代码是跑步。这可能有效,也可能无效;这取决于Some3rdPartyTool 究竟如何使用它的SynchronizationContextHttpContext

请注意,此解决方案中有几个不好的做法:

  • 在 ASP.NET 上使用 Task.Run
  • 从多个线程同时访问同一个HttpContext 实例。
  • 在 ASP.NET 上使用 AsyncContext.Run
  • 阻塞异步代码(由AsyncContext.Run 和大概Some3rdPartyTool 完成。

最后,我再次建议更新/重写/替换Some3rdPartyTool。但是这堆技巧可能会奏效。

【讨论】:

  • 很好的答案,感谢您说得非常清楚!至于Some3rdPartyTool,据我发现它在下面使用 webClient 来调用端点,所以它本质上是同步的(没有阻塞代码)——我最初假设它内部有一些异步是错误的
  • @veljkoz:如果它没有进行异步同步,那么您可以捕获SynchronizationContext.CurrentHttpContext.Current 并使用它们临时将它们设置在其他线程线程上(SynchronizationContext.SetSynchronizationContext /HttpContext.Current),这可以在不使用不同 SynchronizationContext的情况下工作。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2013-10-07
  • 1970-01-01
  • 2012-05-27
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-04-12
相关资源
最近更新 更多