“HttpContext.Current” 属性及其相关内容的跨线程使用

43
我从《Essential ASP.NET with Examples in C#》中读到以下声明:
另一个值得了解的属性是HttpContext类的静态Current属性。该属性始终指向正在服务的请求的HttpContext类的当前实例。如果您正在编写将用于页面或其他管道类并且可能需要访问上下文以进行任何原因的辅助类,则这可能很方便。通过使用静态Current属性来检索上下文,您可以避免将引用传递给辅助类。例如, 列表4-1中显示的类使用上下文的Current属性来访问QueryString并将某些内容打印到当前响应缓冲区。请注意,为了正确初始化此静态属性,调用者必须在原始请求线程上执行,因此,如果您已经生成了其他线程来执行请求期间的工作,则必须自行提供对上下文类的访问。
我想知道加粗部分的根本原因,一件事情导致另一件事情,以下是我的想法:
我们知道一个进程可以拥有多个线程。每个线程都有自己的堆栈。这些线程还可以访问共享内存区域——堆。
那么,据我理解,堆栈是存储该线程所有上下文信息的地方。要使线程访问堆中的东西,它必须使用指针,而指针存储在其堆栈上。
因此,当我们进行一些跨线程调用时,必须确保所有必要的上下文信息从调用线程的堆栈传递到被调用方线程的堆栈。
但我不太确定我是否犯了任何错误。
欢迎您提出任何建议。
谢谢。
补充说明:
这里的堆栈是限于用户堆栈。
3个回答

95
  • HttpContext是一个实例对象,可以在HttpContext.Current中找到它的引用。
  • Thread也是一个实例对象,可以在Thread.CurrentThread中找到它的引用。
  • Thread.CurrentThread是静态的,但在每个线程中引用不同的Thread对象。
  • HttpContext.Current实际上指向 Thread.CurrentThread.ExecutionContext.IllogicalCallContext.HostContext
  • 我们可以从上面的信息得出以下结论:

    1. 因为HttpContext是一个实例对象而不是静态的,所以我们需要其引用才能访问它。
    2. 因为HttpContext.Current实际上指向Thread.CurrentThread的属性,改变Thread.CurrentThread为不同的对象可能会改变HttpContext.Current
    3. 因为Thread.CurrentThread在切换线程时会更改,所以HttpContext.Current 也会在切换线程时更改(在这种情况下,HttpContext.Current变为null)。

    综上所述,什么导致HttpContext.Current不能在新线程中工作?改变Thread.CurrentThread的引用,这会在切换线程时改变HttpContext.Current的引用,从而防止我们访问所需的HttpContext实例。

    再次强调,这里唯一神奇之处就是Thread.CurrentThread在每个线程中引用不同的对象。HttpContext的工作方式与任何其他实例对象相同。由于在同一个AppDomain中的线程可以引用相同的对象,因此我们只需要将HttpContext的引用传递给新线程即可。没有上下文信息需要加载或类似的事情。(将HttpContext传递给其他线程可能会有一些相当严重的潜在问题,但没有任何阻止您这样做的东西)。

    最后,我在研究过程中还发现了一些注意事项:

    1. 在某些情况下,线程的ExecutionContext会从一个线程复制到另一个线程。为什么HttpContext没有被“流动”到新线程?因为HttpContext没有实现ILogicalThreadAffinative接口。只有实现了ILogicalThreadAffinative接口的类才会在执行上下文中流动。

    2. 如果HttpContext没有被“流动”,ASP.NET如何将它从线程移动到线程(线程灵活性)?我不是完全确定,但看起来可能是通过HttpApplication.OnThreadEnter()传递它。


    3
    我猜测原帖发布者不会回来颁发赏金,因为他自从29日以来就没有登录过,所以我认为你应该获得这个非常详尽的答案的自动奖励。点个赞让你排在最前面。 :) - Tudor
    2
    离题一下,但我喜欢你称之为IllogicalContext (有点滑稽,因为多了一个L)。 - The Dag
    有没有绕过或模式可以从另一个线程访问HttpContext? - AlexVPerl
    嗨,Axel,只要您有HttpContext的引用,您应该能够从任何线程访问它。我认为这样做最大的危险是当原始请求线程完成时,ResponseStream被处理。如果您之所以这样做是因为长时间运行的任务,我建议您查看其他模式,如ASP.NET异步控制器。 - Mark Rucker
    1
    @学生 我猜这取决于您尝试在哪里和如何传递它。听起来像是.net正在尝试编排您的上下文。我猜测原因是将引用传递给asp.net应用程序中的HTTP端点。可以说,如果我们只谈论Web API或多线程代码,事情会变得复杂。将两者结合起来,情况变得更加复杂。不幸的是,这意味着您可能需要进行相当多的背景研究,才能找到一个您可以持续利用的答案。 - Mark Rucker
    显示剩余3条评论

    10

    我认为我在这里找到了一个合适的解释:http://odetocode.com/articles/112.aspx

    总结一下,HttpContext.Current 的代码实现看起来像这样:

    public static HttpContext get_Current()
    { 
        return (CallContext.GetData("HtCt") as HttpContext); 
    }
    

    使用 CallContext 作为 线程本地存储 的功能(即每个线程将看到数据的不同副本,不能访问其他线程看到的副本)。 因此,一旦在一个线程上初始化了当前上下文,其他线程对该属性的后续访问将导致 NullReferenceException,因为该属性是仅限于初始线程的线程本地。

    因此,是的,您最初的解释接近正确,就在于数据只对单个线程可见的问题。


    @Mark Rucker:我的意思是在抽象的层面上可见,以向OP表明他在某种程度上是正确的,即只有一个线程可以访问原始实例。 - Tudor

    1

    Current 的后备字段被标记为 ThreadStatic(我猜测),因此它在用户创建的线程中不可用/初始化。

    归根结底,您应该在请求线程中捕获 HttpContext.Current 实例,然后在您的线程中使用该实例,而不是引用 HttpContext.Current


    1
    当前属性的后备字段并非ThreadStatic,尽管它的行为有点像。在回答这个问题之前,我也曾做出了同样的假设,直到我进一步研究它。 - Mark Rucker
    1
    正如已经指出的那样,你的假设是不正确的。此外,在其他线程中使用上下文是“危险”的,因为它的生命周期仅限于请求范围内。在其他线程仍然依赖它的时候,它可能被处理或以其他方式被破坏。最好创建自己所需数据的类型,并将其传递给需要它的代码。 - The Dag

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