前一段时间正好要在某个网页程序上开一个多线程调用多个组件的尝试,这些组件是有其他团队开发的(如:印度/俄罗斯),所以修改它们的代码看起来是不太现实的,但是,令人恼火的是他们的代码中大量的用到了AppContext.Current这个对象(实际上是用了HttpContext.Current.Item来存储的),而一旦异步,HttpContext.Current就不复存在,自然就会不停的报出空引用异常,看起来异步是不太现实的了。
就在无计可施的时候,突然发现有一个叫CallContext的奇怪的类,藏匿在System.Runtime.Remoting.Messaging这个几乎没人用的namespace下面,当然一开始我仅仅是被它的名称所吸引,直译过来不就是调用上下文吗?感觉这个东西能有点作用。于是查阅了msdn,描述如下:
提供与执行代码路径一起传送的属性集。无法继承此类。
一句废话。。。看备注吧:
CallContext 是类似于方法调用的线程本地存储区的专用集合对象,并提供对每个逻辑执行线程都唯一的数据槽。数据槽不在其他逻辑线程上的调用上下文之间共享。当 CallContext 沿执行代码路径往返传播并且由该路径中的各个对象检查时,可将对象添加到其中。
当对另一个 AppDomain 中的对象进行远程方法调用时,CallContext 类将生成一个与该远程调用一起传播的 LogicalCallContext 实例。只有公开 ILogicalThreadAffinative 接口并存储在 CallContext 中的对象被在 LogicalCallContext 中传播到 AppDomain 外部。不支持此接口的对象不在 LogicalCallContext 实例中与远程方法调用一起传输。
似乎有那么点意思,不过逻辑线程的定义似乎有那么点模棱两可,不过,也提到了一个接口ILogicalThreadAffinative,再看看这个接口定义了什么,一看成员定义。。。没有成员。。。一个空接口,汗了一把,还是看看msdn上如何描述的吧:
标记可以在 LogicalCallContext 中传播到 AppDomain 外部的对象。
强调了在remoting中的作用,再看看备注:
当对另一个 AppDomain 中的对象进行远程方法调用时,当前的 CallContext 类生成一个将与该调用一起传播到远程位置的 LogicalCallContext。只有公开 ILogicalThreadAffinative 接口并存储在 CallContext 中的对象被传播到 AppDomain 外部。不支持此接口的对象不在 LogicalCallContext 实例中与远程方法调用一起传输。
也是强调在remoting中的作用,但是可以想象,基本上是CallContext中用了类似is ILogicalThreadAffinative的方式,来区别对待不同的对象,对于符合这个接口的将被放到LogicalCallContext,而不符合的另外处理。
从文档角度,似乎已经没有什么进展了,这时候,突然想起来一个以前看过的很有趣的类型ExecutionContext,namespace是System.Threading,看起来就是多线程准备的,不过,msdn上的例子就说了如何控制传递权限对象的问题,并没有说到如何传递普通对象。
一时想到所谓的LogicalCallContext会不会在ExecutionContext中存在哪?查了一下msdn,看到备注:
ExecutionContext 类为与执行的逻辑线程相关的所有信息提供单个容器。这包括安全上下文、调用上下文和同步上下文。
ExecutionContext 类提供的功能让户代码可以在用户定义的异步点之间捕获和传输此上下文。公共语言运行库确保在托管进程内运行库定义的异步点之间一致地传输 ExecutionContext。
执行上下文是 COM 单元的托管等效项。在应用程序域中,每当传输线程时都必须传输整个执行上下文。在由 Thread..::.Start 方法、大多数线程池操作和通过 Windows 消息泵进行的 Windows 窗体线程封送处理所导致的传输过程中,将会出现这种情况。在不安全的线程池操作(如 UnsafeQueueUserWorkItem 方法)中不会出现这种情况,原因是不安全的线程池操作不会传输压缩堆栈。每当压缩堆栈流动时,托管的主体、同步、区域设置和用户上下文也随之流动。ExecutionContext 类提供 Capture 和 CreateCopy 方法以获取执行上下文,并提供 Run 方法以设置当前线程的执行上下文。
与某个线程相关联的 ExecutionContext 无法在另一个线程上进行设置。尝试这样做会导致引发异常。若要将 ExecutionContext 从一个线程传播到另一个线程,请制作 ExecutionContext 的副本。
ExecutionContext 在内部存储与 LogicalCallContext 相关联的所有数据。这使得可以在复制和传输 ExecutionContext 时传播 LogicalCallContext 数据。
果然,ExecutionContext中有LogicalCallContext的数据,并且很好的说明了,无论是Thread.Start还是用线程池大多数操作,ExecutionContext都会自动将这些数据传递给那些线程(关于ThreadPool.UnsafeQueueUerWorkItem方法相信用的人应该不多),看起来演员们都到齐了,马上可以演出一场多线程的好戏了。
首先是起着关键作用ExectionContext,也许我们的代码中没必要出现它的身影,但是那仅仅是因为.net类库的方法,为我们很好封装了这个功能,没有它,想在一个线程中把一个对象告诉另一个线程,就只有通过堆了。
其次,LogicalCallContext,在众多ExecutionContext传播的对象中,很多是我们无法简单的利用的(总不能为了传播一个对象,去定义一个自定义的权限吧),而LogicalCallContext就是自定义对象的最好的载体。
最后,剩下的问题就是如何读写这个LogicalCallContext的问题了,也就是终于轮到CallContext出场了。看一下CallContext为我们准备了些什么方法:GetData, SetData, LogicalGetData, LogicalSetData, FreeNamedDataSlot, GetHeader, SetHeader以及HostContext属性。
第一焦点,当然是GetData和SetData这两个方法,做了个简单的测试,发现这两个方法,确实就是通过ILogicalThreadAffinative接口来决定是否要把对象发给新线程的,而删除这个数据的方法就是FreeNamedDataSlot,现在只要为每一个要在多线程中共用的对象加上一个空接口,并且在多线程开始前在主线程中把对象设置进去,然后在其他线程中再取出来就可以把问题搞定了。多线程部分结束后,不要忘记用FreeNamedDataSlot去删除一下。
不知道大家注意到没有,出了GetData和SetData外,还有一对LogicalGetData和LogicalSetData,这两个是干什么用的哪?是不是和LogicalCallContext的Logical有什么关系哪?
又把前面的那个简单的实验做了一下,只不过用了LogicalGetData和LogicalSetData这两个方法,结论是无论是否实现ILogicalThreadAffinative接口,对象都可以在新线程内被访问到,也就是说现在可以传播任何数据给新线程,包括.net定义的string,int等基础类型,这个方案已经接近完美了。
回过头来看看我的任务吧,只需要修改AppContext.Current属性的实现,并且在多线程开始之前和之后做一个小小的处理,其他的组件就可以原封不动的并发的跑在各自的线程上。
工作上的事情就到此为止了。
在来说说CallContext.HostContext属性是干什么的,经过简单的测试,发现在Asp.net程序中,这个HostContext里面放的就是HttpContext的实例,ms也够偷懒的。其实ms只要做一个很小的修改,Asp.net的多线程就不用这么麻烦了,只需要HttpContext实现一下ILogicalThreadAffinative接口,无论新开多少个线程,到那边都能访问到HttpContext.Current了,当然ms没有这么做也是有原因的,一旦HttpContext被传播到其他线程,那么asp.net就很难控制HttpContext对象的生命周期,而HttpContext对象又引用这HttpRequest对象,带来的副作用可能要比想象的大得多。