在 ASP.NET 中保留 HttpContext.Current 的同时并行化同步任务

Parallelizing synchronous tasks while retaining the HttpContext.Current in ASP.NET

我在 SO 中搜索了答案,但发现 none 与手头的问题有关,尽管 强调了“为什么”,但没有解决它。

我有一个 REST 端点需要从其他端点收集数据 - 在这样做时,它访问 HttpContext(设置身份验证、headers 等......所有这些都是由第 3 方完成的lib 我无权访问)。

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

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

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

有治疗方法吗?
我们想针对单个请求进行优化,目前对最大化并行用户不感兴趣。

Unfortunately, this library for service communication is made to be synchronous, and we want to parallelize its use.

throws null exception when HttpContext isn't set:

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

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

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

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

但是,您有很小的机会可以通过添加更危险的代码来使这种方式正常工作。

因此,这是您需要了解的信息:

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

现在,您 可以 捕获 SynchronizationContext.Current 并将其安装在线程池线程上,但除了非常危险之外,它不会实现您的实际目标(并行化),因为 AspNetSynchronizationContext 一次只允许一个线程进入。第 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

综上,再次推荐updating/rewriting/replacingSome3rdPartyTool。但是这一堆技巧 可能 有用。