ASP.NET中的HttpContext.Current在Task.Run中的使用

21

我有一个代码示例,它用于ASP.NET MVC应用程序。 这段代码的目的是创建“fire and forget”请求以排队一些长时间运行的操作。

public JsonResult SomeAction() {
   HttpContext ctx = HttpContext.Current;            

   Task.Run(() => {
       HttpContext.Current = ctx;
       //Other long running code here.
   });

   return Json("{ 'status': 'Work Queued' }");
}

我知道这不是在异步代码中处理HttpContext.Current的好方法,但目前我们的实现不允许我们做其他事情。我想了解一下这段代码到底有多危险...

问题: 在Task.Run内设置HttpContext是否理论上可能会将上下文设置为完全不同的请求?

我认为是可能的,但我不确定。我的理解是: 请求1由线程池中的Thread1处理,然后在Thread1处理完另一个请求(请求2)之后,Task.Run内的代码将上下文从请求1设置为请求2。

也许我错了,但是我对ASP.NET内部的了解不足以正确理解它。

谢谢!


3
直接从HttpContext中获取所需信息是否比传入整个HttpContext更简单?(我知道这并没有回答你的问题,但我很好奇为什么需要整个Context。) - vcsjones
2
是的,这是更正确的方式,但不幸的是,目前我无法更改它。我们的代码在业务逻辑深处具有HttpContext.Current访问权限,更改它需要付出巨大的努力,而目前我们还没有做好准备。 - Alex Dn
1
与您的问题无关 - 在 Web 上下文中运行长时间任务并不是一个好主意 - 服务器可能会重新启动,而线程池中只有那么多线程 - 一旦线程用尽,您将停止服务请求。您是否考虑过像 HangFire 或 Quartz 这样的解决方案? - Ondrej Svejdar
@OndrejSvejdar,你是正确的,但这就是我们目前拥有的,现在不能承受改变架构的成本。 - Alex Dn
你有没有在 //其他长时间运行的代码这里使用 "await"? - Ondrej Svejdar
显示剩余2条评论
2个回答

14

让我向您介绍一些内部信息:

public static HttpContext Current
{
    get { return ContextBase.Current as HttpContext; }
    set { ContextBase.Current = value; }
}

internal class ContextBase
{
    internal static object Current
    {
        get { return CallContext.HostContext; }
        set { CallContext.HostContext = value; }
    }
}

public static object HostContext
{
    get 
    {
        var executionContextReader = Thread.CurrentThread.GetExecutionContextReader();
        object hostContext = executionContextReader.IllogicalCallContext.HostContext;
        if (hostContext == null)
        {
            hostContext = executionContextReader.LogicalCallContext.HostContext;
        }
        return hostContext;
   }
   set
   {
        var mutableExecutionContext = Thread.CurrentThread.GetMutableExecutionContext();
        if (value is ILogicalThreadAffinative)
        {
            mutableExecutionContext.IllogicalCallContext.HostContext = null;
            mutableExecutionContext.LogicalCallContext.HostContext = value;
            return;
        }
        mutableExecutionContext.IllogicalCallContext.HostContext = value;
        mutableExecutionContext.LogicalCallContext.HostContext = null;
   }
}

所以

var context = HttpContext.Current;

等于(伪代码)

var context = CurrentThread.HttpContext;

在你的 Task.Run 内部会发生类似这样的事情

CurrentThread.HttpContext= context;

Task.Run 会使用线程池中的线程启动一个新任务。所以,您正在告诉我们,您的新线程的 "HttpContext 属性" 是对启动线程的 "HttpContext 属性" 的引用- 到目前为止还不错(好吧,在启动线程完成后,您将面临所有 NullReference/Dispose 异常问题)。问题在于如果在您的

//Other long running code here.

你有这样的声明:

var foo = await Bar();
一旦您使用 await,当前线程将返回到线程池中,然后在 IO 完成后,您将从线程池中获取新线程 - 想知道它的 "HttpContext 属性" 是什么吗?我不知道 :) 最可能的是您最终会遇到 NullReferenceException。

非常感谢您提供如此详细的答案!根据您所写的,我理解在技术上它是可以发生的(Request2将获取Request1的上下文),但最有可能会出现空引用和处理异常,因为实际上Request1已经完成并且ASP.NET已经“清除”了它的上下文...我是否正确理解了您的答案?谢谢! - Alex Dn

5
在这里,你会遇到一个问题,即当请求完成时,HttpContext将被释放。由于你没有等待Task.Run的结果,因此在任务中使用HttpContext会创建一种竞争条件。
我非常确定你的任务唯一可能遇到的问题是NullReferenceException或ObjectDisposedException。我不认为你会意外地窃取另一个请求的上下文。
另外,除非你在任务中处理并记录异常,否则你的快速启动和忘记会抛出异常,而你将永远不会知道发生了什么。
请查看HangFire或考虑使用消息队列来处理来自单独进程的后端作业。

谢谢您的回答。我知道竞态条件和可能的NullReference / Disposed异常。但我想了解为什么您认为无法窃取另一个请求上下文的技术原因...据我所知,HttpContext.Current通过线程ID管理上下文,因此如果两个请求将接收到具有相同ID的线程,则HttpContext.Current可以从两个请求返回对同一对象的引用。 - Alex Dn

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接