MarshalByRefObject 陷阱——总结使用 MarshalByRefObject 需要小心的地方

  近日在做一个项目,涉及到跨 AppDomain 的处理,在使用 MarshalByRefObject 对象时遇到并解决了一些“诡异”的问题,故在此总结一下,与大家分享,也留日后备忘。

  跨域传递数据时,只有两类数据可以传递:可序列化类型、MarshalByRefObject 派生类型。前者包括基本类型、使用了 SerializeAttribute 的类型、实现了ISerializable 接口的类型。在跨域传递数据时,可序列化类型的数据将通过序列化传递数据副本;MarshalByRefObject 派生类型则只是在请求数据AppDomain创建一个指向目标AppDomain中真实对象的透明代理。

跨域传递 MarshalByRefObject
1 public static void Main(string[] args)
2 {
3 Console.WriteLine("当前域:{0}", AppDomain.CurrentDomain.FriendlyName);
4 AppDomain domain = AppDomain.CreateDomain("RemoteDomain");
5
6 domain.DoCallBack(delegate()
7 {
8 //在远程域创建数据对象;
9   AppDomain.CurrentDomain.SetData("DT", new DataObject());
10 });
11
12 //获取远程域中创建的数据对象;
13   DataObject dt = (DataObject)domain.GetData("DT");//dt实际是 System.Runtime.Remoting.Proxies.__TransparentProxy
14   dt.DoSomething();
15
16 Console.ReadLine();
17 }
18 }
19
20 public class DataObject : MarshalByRefObject
21 {
22 public void DoSomething()
23 {
24 Console.WriteLine("当前域:{0}", AppDomain.CurrentDomain.FriendlyName);
25 }
26 }

   以上的规则看起来似乎很简单,进行跨域传递对象似乎也没什么特别的地方。但如果真的认为毫无特别之处,那你就真的错了。下面我把我曾犯过的一些错误:

  • 错误1:清空上面代码中对创建的 DataObject 的所有引用,再执行 GC.Collect() 操作强制执行垃圾回收,将会销毁创建的 DataObject 对象。

   实际上并不会产生预期的 DataObject 在执行 GC.Collect() 之后立即被回收的效果。我们将上面的代码修改如下:

清空 DataObject 执行垃圾回收
1 class DemoCrossAppDomain
2 {
3 public static void Main(string[] args)
4 {
5 Console.WriteLine("当前域:{0}", AppDomain.CurrentDomain.FriendlyName);
6 AppDomain domain = AppDomain.CreateDomain("RemoteDomain");
7
8 domain.DoCallBack(delegate()
9 {
10 //在远程域创建数据对象;
11 AppDomain.CurrentDomain.SetData("DT", new DataObject());
12 });
13
14 //获取远程域中创建的数据对象;
15 DataObject dt = (DataObject)domain.GetData("DT");//dt实际是 System.Runtime.Remoting.Proxies.__TransparentProxy
16 dt.DoSomething();
17
18 domain.SetData("DT", null);
19 dt = null;
20 GC.Collect();
21
22 Console.ReadLine();
23 }
24 }
25
26 public class DataObject : MarshalByRefObject
27 {
28 public DataObject()
29 {
30 Console.WriteLine("创建了 DataOjbect…… 当前域:{0}", AppDomain.CurrentDomain.FriendlyName);
31 }
32
33 ~DataObject()
34 {
35 Console.WriteLine("销毁 DataObject ……当前域:{0}", AppDomain.CurrentDomain.FriendlyName);
36 }
37
38 public void DoSomething()
39 {
40 Console.WriteLine("当前域:{0}", AppDomain.CurrentDomain.FriendlyName);
41 }
42 }

  除非你立即执行 AppDomain.Unload(domain); 卸载远程域,才会立即销毁 DataObject,此时能够看到析构函数中的输出。

  为什么呢? DataObject 似乎已经没有任何引用了,难道不是吗?这实际上还有引用:domain 中进行跨域通讯的 Remoting 基础结构在持有 DataObject ,该结构是负责将请求数据的远程AppDomain 中的透明代理的调用请求转发给真实的对象的,MarshalByRefOjbect 通过生命期服务 LifeService 控制该结构的生命周期。DataObject 对象之所以未被销毁是因为该基础结构的生命周期尚未过期。默认情况下,如果不再有任何调用操作后大约15分钟将销毁该结构,DataOjbect 才可以被垃圾回收。对生命周期的控制请参考 MSDN 关于.NET Remoting 的生命期服务相关内容。

  • 错误2:通过透明代理访问一个基本类型的属性(如 int)或者可序列化的对象类型的属性不会产生对真实对象所在的远程域的远程调用。
通过透明代理访问属性
1 public class DataObject : MarshalByRefObject
2 {
3 // ID;
4 public int _id;
5 /// <summary>
6 /// ID;
7 /// 读写属性;
8 /// </summary>
9 public int ID
10 {
11 get { return _id; }
12 set { _id = value; }
13 }
14
15 // data1;
16 private InnerData1 _data1;
17 /// <summary>
18 /// data1;
19 /// 读写属性;
20 /// </summary>
21 public InnerData1 Data1
22 {
23 get { return _data1; }
24 set { _data1 = value; }
25 }
26
27 // data2;
28 private InnerData2 _data2;
29 /// <summary>
30 /// data2;
31 /// 读写属性;
32 /// </summary>
33 public InnerData2 Data2
34 {
35 get { return _data2; }
36 set { _data2 = value; }
37 }
38
39
40 public DataObject()
41 {
42 Console.WriteLine("创建了 DataOjbect…… 当前域:{0}", AppDomain.CurrentDomain.FriendlyName);
43 }
44
45 ~DataObject()
46 {
47 Console.WriteLine("销毁 DataObject ……当前域:{0}", AppDomain.CurrentDomain.FriendlyName);
48 }
49
50 public void DoSomething()
51 {
52 Console.WriteLine("当前域:{0}", AppDomain.CurrentDomain.FriendlyName);
53 }
54 }
55
56 [Serializable]
57 public class InnerData1
58 {
59 public string Data { get; set; }
60 }
61
62 public class InnerData2 : MarshalByRefObject
63 {
64 public string Data { get; set; }
65 }

 

 

    这也是一个陷阱。实际上,无论属性的类型是什么,通过透明代理对对象的任何访问,包括通过 GET/SET 访问器访问属性、访问公开的成员、调用方法等,都会产生跨域调用,即使属性/成员的类型是可序列化的,透明代理也不会保存其属性值的副本,如上面的修改后的 DataObject 的所有的属性,包括 public int _id; 成员。运行以上的测试代码很容易验证,不做详细叙述了。

   知晓这一点所具备的指导意义是:当频繁访问一个大数据量而不变化的序列化类型的属性时,更好的做法是一次访问后在本地做缓存,而不要直接通过透明代理访问属性,因为跨域调用时频繁地对大数据进行序列化和反序列化将会消耗你宝贵的计算资源。

 

  跨域操作表面简单,而底层实际是 .NET Remoting 实现的,这其中的机制是比较复杂的。透明代理虽然给我们带来了巨大的便利,屏蔽了大量的复杂的底层细节的同时,也带来了让人容易在不了解底层机制的情形下误用而产生难以察觉的 BUG 的风险。当你需要进行跨 AppDomain 操作是,深入了解 .NET Remoting 机制对于提升系统的健壮性是很重要的。

posted @ 2010-06-14 00:51  haiq  阅读(1606)  评论(1)    收藏  举报