c#发展

注册

 

发新话题 回复该主题

为什么HttpContextAccess [复制链接]

1#
北京手足癣医院电话 http://baidianfeng.39.net/a_zhiliao/210410/8833646.html
前言

周五在群里面有小伙伴问,ASP.NETCore这个HttpContextAccessor为什么改成了这个样子?在印象中,这已经是第三次遇到有小伙伴问这个问题了,特意来写一篇记录,来回答一下这个问题。

聊一聊历史

关于HttpContext其实我们大家都不陌生,它封装了HttpRequest和HttpResponse,在处理Http请求时,起着至关重要的作用。

CallContext时代

那么如何访问HttpContext对象呢?回到await/async出现以前的ASP.NET的时代,我们可以通过HttpContext.Current方法直接访问当前Http请求的HttpContext对象,因为当时基本都是同步的代码,一个Http请求只会在一个线程中处理,所以我们可以使用能在当前线程中传播的CallContext.HostContext来保存HttpContext对象,它的代码长这个样子。

namespaceSystem.Web.Hosting{usingSystem.Web;usingSystem.Web.Configuration;usingSystem.Runtime.Remoting.Messaging;usingSystem.Security.Permissions;internalclassContextBase{internalstaticObjectCurrent{get{//CallContext在不同的线程中不一样returnCallContext.HostContext;}[SecurityPermission(SecurityAction.Demand,Unrestricted=true)]set{CallContext.HostContext=value;}}......}}}

一切都很美好,但是后面微软在C#为了进一步增强增强了异步IO的性能,从而实现的stackless协程,加入了await/async关键字(感兴趣的小伙伴可以阅读黑洞的这一系列文章),同一个方法内的代码await前与后不一定在同一个线程中执行,那么就会造成在await之后的代码使用HttpContext.Current的时候访问不到当前的HttpContext对象,下面有一段这个问题简单的复现代码。

//设置当前线程HostContextCallContext.HostContext=newDictionarystring,string{["ContextKey"]="ContextValue"};//await前,可以正常访问Console.Write("[{Thread.CurrentThread.ManagedThreadId}]awaitbefore:");Console.WriteLine(((Dictionarystring,string)CallContext.HostContext)["ContextKey"]);awaitTask.Delay();//await后,切换了线程,无法访问Console.Write("[{Thread.CurrentThread.ManagedThreadId}]awaitafter:");Console.WriteLine(((Dictionarystring,string)CallContext.HostContext)["ContextKey"]);

可以看到await执行之前HostContext是可以正确的输出赋值的对象和数据,但是await以后的代码由于线程从16切换到29,所以访问不到上面代码给HostContext设置的对象了。

AsyncLocal时代

为了解决这个问题,微软在.NET4.6中引入了AsyncLocalT类,后面重新设计的ASP.NETCore自然就用上了AsyncLocalT来存储当前Http请求的HttpContext对象,也就是开头截图的代码一样,我们来尝试一下。

varasyncLocal=newAsyncLocalDictionarystring,string();//设置当前线程HostContextasyncLocal.Value=newDictionarystring,string{["ContextKey"]="ContextValue"};//await前,可以正常访问Console.Write("[{Thread.CurrentThread.ManagedThreadId}]awaitbefore:");Console.WriteLine(asyncLocal.Value["ContextKey"]);awaitTask.Delay();//await后,切换了线程,可以访问Console.Write("[{Thread.CurrentThread.ManagedThreadId}]awaitafter:");Console.WriteLine(asyncLocal.Value["ContextKey"]);

没有任何问题,线程从16切换到了17,一样的可以访问。对AsyncLocal感兴趣的小伙伴可以看黑洞的这篇文章。简单的说就是AsyncLocal默认会将当前线程保存的上下对象在发生await的时候传播到后续的线程上。这看起来就非常的美好了,既能开开心心的用await/async又不用担心上下文数据访问不到,那为什么ASP.NETCore的后续版本需要修改HttpContextAccesor呢?我们自己来实现ContextAccessor,大家看下面一段代码。

//给Context赋值一下varaccessor=newContextAccessor();accessor.Context="ContextValue";Console.WriteLine("[{Thread.CurrentThread.ManagedThreadId}]Main-1:{accessor.Context}");//执行方法awaitMethod();//再打印一下Console.WriteLine("[{Thread.CurrentThread.ManagedThreadId}]Main-2:{accessor.Context}");asyncTaskMethod(){//输出Context内容Console.WriteLine("[{Thread.CurrentThread.ManagedThreadId}]Method-1:{accessor.Context}");awaitTask.Delay();//注意!!!,我在这里将Context对象清空Console.WriteLine("[{Thread.CurrentThread.ManagedThreadId}]Method-2:{accessor.Context}");accessor.Context=null;Console.WriteLine("[{Thread.CurrentThread.ManagedThreadId}]Method-3:{accessor.Context}");}//实现一个简单的ContextAccessorpublicclassContextAccessor{staticAsyncLocalstring_contextCurrent=newAsyncLocalstring();publicstringContext{get=_contextCurrent.Value;set=_contextCurrent.Value=value;}}

奇怪的事情就发生了,为什么明明在Method中把Context对象置为null了,Method-3中已经输出为null了,为啥在Main-2输出中还是ContextValue呢?

AsyncLocal使用的问题

其实这已经解答了上面的问题,就是为什么在ASP.NETCore6.0中的实现方式突然变了,有这样一种场景,已经当前线程中把HttpContext置空了,但是其它线程仍然能访问HttpContext对象,导致后续的行为可能不一致。

那为什么会造成这个问题呢?首先我们得知道AsyncLocal是如何实现的,这里我就不在赘述,详细可以看我前面给的链接(黑洞大佬的文章)。这里只简单的说一下,我们只需要知道AsyncLocal底层是通过ExecutionContext实现的,每次设置Value时都会用新的Context对象来覆盖原有的,代码如下所示(有删减)。

publicsealedclassAsyncLocalT:IAsyncLocal{publicTValue{[SecuritySafeCritical]get{//从ExecutionContext中获取当前线程的值objectobj=ExecutionContext.GetLocalValue(this);return(obj==null)?default(T)T)obj;}[SecuritySafeCritical]set{//设置值ExecutionContext.SetLocalValue(this,value,m_valueChangedHandler!=null);}}}......publicsealedclassExecutionContext:IDisposable,ISerializable{internalstaticvoidSetLocalValue(IAsyncLocallocal,objectnewValue,boolneedChangeNotifications){varcurrent=Thread.CurrentThread.GetMutableExecutionContext();objectpreviousValue=null;if(previousValue==newValue)return;varnewValues=current._localValues;//无论是AsyncLocalValueMap.Create还是newValues.Set//都会创建一个新的IAsyncLocalValueMap对象来覆盖原来的值if(newValues==null){newValues=AsyncLocalValueMap.Create(local,newValue,treatNullValueAsNonexistentneedChangeNotifications);}else{newValues=newValues.Set(local,newValue,treatNullValueAsNonexistentneedChangeNotifications);}current._localValues=newValues;......}}

接下来我们需要避开await/async语法糖的影响,反编译一下IL代码,使用C#1.0来重新组织代码(使用ilspy或者dnspy之类都可以)。可以看到原本的语法糖已经被拆解成stackless状态机,这里我们重点

分享 转发
TOP
发新话题 回复该主题