Warm tip: This article is reproduced from serverfault.com, please click

c#-并行同步任务,同时在ASP.NET中保留HttpContext.Current

(c# - Parallelizing synchronous tasks while retaining the HttpContext.Current in ASP.NET)

发布于 2020-11-02 22:57:06

我一直在搜寻SO的答案,但没有找到与手头问题有关的答案,尽管这是“为什么”的问题,但并未解决。

我有一个REST端点,需要从其他端点收集数据-这样,它就可以访问HttpContext(设置身份验证,标头等...所有操作都由我无法访问的第三方库完成)。

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

在以下示例(抽象)代码中,问题是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... 
}

有办法解决吗?
我们希望针对单个请求进行优化,目前不希望最大化并行用户。

Questioner
veljkoz
Viewed
11
Stephen Cleary 2020-12-01 00:39:09

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

当未设置HttpContext时,将引发null异常:

明显的答案(HttpContext.Current = parentContext)无法工作,因为在其下有一些异步代码(无论出于何种原因),这有时会导致它不返回到同一线程,并且基本上放弃了Context,再次导致null

在示例代码注释中,你的问题有很重要的一部分。:)

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

这里更隐蔽的问题是该库具有同步API,并且正在执行异步同步-但以某种方式没有死锁(?)。在这一点上,我必须诚实地说,最好的方法是修复库:让供应商修复它,或提交PR,或者如果需要的话,只重写它。

但是,极有可能通过添加甚至更危险的代码来使这种工作正常进行。

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

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

现在,你可以捕获SynchronizationContext.Current并将其安装在线程池线程上,但是除了非常危险之外,它还不能实现你的实际目标(并行化),因为一次AspNetSynchronizationContext只能允许一个线程进入。第三方代码的第一部分将能够并行运行,但是排队的所有内容AspNetSynchronizationContext将一次运行一个线程。

因此,我想到进行此工作的唯一方法是使用自己的自定义SynchronizationContext,该自定义在同一线程上恢复并HttpContext.Current在该线程上进行设置我有一个AsyncContext可以被用于此:

[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使用SynchronizationContextand和HttpContext

请注意,此解决方案有几种不良做法:

  • Task.Run在ASP.NET上使用
  • HttpContext从多个线程同时访问同一实例。
  • AsyncContext.Run在ASP.NET上使用
  • 阻塞异步代码(由AsyncContext.Run,也可能Some3rdPartyTool

最后,我再次建议更新/重写/替换Some3rdPartyTool但是,这堆黑客也许行得通。