AppDomain 和 MarshalByRefObject 生命周期:如何避免 RemotingException?
Posted
技术标签:
【中文标题】AppDomain 和 MarshalByRefObject 生命周期:如何避免 RemotingException?【英文标题】:AppDomain and MarshalByRefObject life time : how to avoid RemotingException? 【发布时间】:2011-01-25 11:55:35 【问题描述】:当 MarshalByRef 对象从 AppDomain (1) 传递到另一个 (2) 时,如果您等待 6 分钟,然后在第二个 AppDomain (2) 中对其调用方法,您将收到 RemotingException:
System.Runtime.Remoting.RemotingException: 对象 [...] 已断开连接或 服务器上不存在。
关于这个问题的一些文档:
http://blogs.microsoft.co.il/blogs/sasha/archive/2008/07/19/appdomains-and-remoting-life-time-service.aspx http://blogs.msdn.com/cbrumme/archive/2003/06/01/51466.aspx - 实例生命周期,cbrumme 说“我们应该解决这个问题。” :(如果我错了,请纠正我:如果 InitializeLifetimeService 返回 null,那么当 AppDomain 2 Unloaded 时,即使收集了代理,也只能在 AppDomain 1 中收集对象?
有没有办法禁用生命周期并保持代理(在 AppDomain 2 中)和对象(在 AppDomain1 中)活动,直到代理完成?也许与 ISponsor...?
【问题讨论】:
【参考方案1】:对于那些希望更深入地了解 .NET Remoting Framework 的人,我建议将标题为 "Remoting Managing the Lifetime of Remote .NET Objects with Leasing and Sponsorship" 的文章发布在MSDN Magazine December 2003 issue。
【讨论】:
是的,我们已经读过了。它没有说明为什么没有要求赞助商续订租约的信息。【参考方案2】:这里有两种可能的解决方案。
单例方法:重写 InitializeLifetimeService
作为原始海报链接的Sacha Goldshtein points out in the blog post,如果您的Marshaled 对象具有单例语义,您可以覆盖InitializeLifetimeService
:
class MyMarshaledObject : MarshalByRefObject
public bool DoSomethingRemote()
// ... execute some code remotely ...
return true;
[SecurityPermissionAttribute(SecurityAction.Demand, Flags = SecurityPermissionFlag.Infrastructure)]
public override object InitializeLifetimeService()
return null;
但是,正如 user266748 在another answer 中指出的那样
如果每次都创建这样的对象,该解决方案将不起作用 客户端连接自己,因为他们永远不会被 GCed 和你的 内存消耗会不断增加,直到您停止 服务器或由于没有更多内存而崩溃
基于类的方法:使用 ClientSponsor
更通用的解决方案是使用ClientSponsor
来延长类激活远程对象的寿命。链接的 MSDN 文章有一个有用的起始示例,您可以参考:
using System;
using System.Runtime.Remoting;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Channels.Tcp;
using System.Runtime.Remoting.Lifetime;
namespace RemotingSamples
class HelloClient
static void Main()
// Register a channel.
TcpChannel myChannel = new TcpChannel ();
ChannelServices.RegisterChannel(myChannel);
RemotingConfiguration.RegisterActivatedClientType(
typeof(HelloService),"tcp://localhost:8085/");
// Get the remote object.
HelloService myService = new HelloService();
// Get a sponsor for renewal of time.
ClientSponsor mySponsor = new ClientSponsor();
// Register the service with sponsor.
mySponsor.Register(myService);
// Set renewaltime.
mySponsor.RenewalTime = TimeSpan.FromMinutes(2);
// Renew the lease.
ILease myLease = (ILease)mySponsor.InitializeLifetimeService();
TimeSpan myTime = mySponsor.Renewal(myLease);
Console.WriteLine("Renewed time in minutes is " + myTime.Minutes.ToString());
// Call the remote method.
Console.WriteLine(myService.HelloMethod("World"));
// Unregister the channel.
mySponsor.Unregister(myService);
mySponsor.Close();
生命周期管理在 Remoting API(described quite well here on MSDN)中的工作方式毫无价值。我引用了我认为最有用的部分:
远程生命周期服务将租约与每个服务相关联, 并在其租用时间到期时删除服务。一生 服务可以承担传统分布式垃圾的功能 收集器,并且它也可以在每个客户端的数量时很好地调整 服务器增加。
每个应用程序域都包含一个负责的租用管理器 用于控制其域内的租约。所有租约都经过审查 定期为过期的租约时间。如果租约已到期,一个或 更多的租赁发起人被调用,并有机会 续租。如果没有赞助商决定续租, 租约管理器删除租约,对象可以被收集 垃圾收集器。租赁管理器维护一个租赁列表,其中包含 租赁按剩余租赁时间排序。最短的租约 剩余时间存储在列表顶部。远程处理 生命周期服务将一个租约与每个服务相关联,并删除一个 租期到期时提供服务。
【讨论】:
这个答案被低估了 Microsoft 引入了ClientSponsor
类来替换 SponsporshipManager
(原始类)。 未记录的问题是赞助商也有租约,所以当它到期时,它不能再响应续订请求。 ClientSponsor
使用未到期的租约创建自己,因此它会按预期续订赞助对象。同样未记录的是ClientSponsor
可以注册多个对象。【参考方案3】:
如果您想在远程对象被垃圾回收后重新创建远程对象,而无需创建ISponsor
类或给它无限的生命周期,您可以在捕获RemotingException
时调用远程对象的虚拟函数.
public static class MyClientClass
private static MarshalByRefObject remoteClass;
static MyClientClass()
CreateRemoteInstance();
// ...
public static void DoStuff()
// Before doing stuff, check if the remote object is still reachable
try
remoteClass.GetLifetimeService();
catch(RemotingException)
CreateRemoteInstance(); // Re-create remote instance
// Now we are sure the remote class is reachable
// Do actual stuff ...
private static void CreateRemoteInstance()
remoteClass = (MarshalByRefObject)AppDomain.CurrentAppDomain.CreateInstanceFromAndUnwrap(remoteClassPath, typeof(MarshalByRefObject).FullName);
【讨论】:
哼,这是一个相当肮脏的解决方案。【参考方案4】:我创建了一个在销毁时断开连接的类。
public class MarshalByRefObjectPermanent : MarshalByRefObject
public override object InitializeLifetimeService()
return null;
~MarshalByRefObjectPermanent()
RemotingServices.Disconnect(this);
【讨论】:
【参考方案5】:[SecurityPermissionAttribute(SecurityAction.Demand, Flags = SecurityPermissionFlag.Infrastructure)]
public override object InitializeLifetimeService()
return null;
我已经对此进行了测试,它的工作正常,当然,必须知道代理永远存在,直到您自己进行 GC-ing。但我的情况是,使用连接到我的主应用程序的插件工厂,没有内存泄漏或类似的东西。我只是确定,我正在实现 IDisposable 并且它工作正常(我可以说,因为一旦工厂被正确处理,我加载的 dll(在工厂中)可以被覆盖)
编辑:如果您通过域冒泡事件,请将这行代码添加到创建代理的类中,否则您的冒泡也会抛出;)
【讨论】:
【参考方案6】:您可以尝试实现 IObjectReference 的可序列化单例 ISponsor 对象。 GetRealObject 实现(来自 IObjectReference 应在 context.State 为 CrossAppDomain 时返回 MySponsor.Instance,否则返回自身。MySponsor.Instance 是自初始化、同步 (MethodImplOptions.Synchronized)、单例。更新实现(来自 ISponsor)应检查静态 MySponsor.IsFlaggedForUnload 并在标记为 unload/AppDomain.Current.IsFinalizingForUnload() 时返回 TimeSpan.Zero,否则返回 LifetimeServices.RenewOnCallTime。
要附加它,只需获取一个 ILease 和 Register(MySponsor.Instance),由于 GetRealObject 实现,它将转换为 AppDomain 内的 MySponsor.Instance 集。
要停止赞助,请重新获取 ILease 和 Unregister(MySponsor.Instance),然后通过跨 AppDomain 回调 (myPluginAppDomain.DoCallback(MySponsor.FlagForUnload)) 设置 MySponsor.IsFlaggedForUnload。
这应该让您的对象在另一个 AppDomain 中保持活动状态,直到取消注册调用、FlagForUnload 调用或 AppDomain 卸载。
【讨论】:
【参考方案7】:在这里查看答案:
http://social.msdn.microsoft.com/Forums/en-US/netfxremoting/thread/3ab17b40-546f-4373-8c08-f0f072d818c9/
这基本上是说:
[SecurityPermissionAttribute(SecurityAction.Demand, Flags = SecurityPermissionFlag.Infrastructure)]
public override object InitializeLifetimeService()
return null;
【讨论】:
对象将保持连接状态,如果您有许多远程对象,您将很快耗尽资源。我的问题的第二部分是关于 InitializeLifetimeService 返回 null。 其实我只有一个远程对象。该操作可能需要很长时间才能完成(根据用户数据,可能需要数天......)。使用此实现不会耗尽资源 - 我已经测试和重新测试。 嗯...有几个人对此表示反对,但没有说明他们为什么这样做。虽然这可能毫无意义,但很高兴知道原因(从文明的角度......)。此外,这个解决方案在现实生活中的商业应用中效果很好,我不是随便拿的。 我猜反对票是因为您的解决方案非常极端。当然它适用于您的现实生活中的商业应用程序,但这只是因为您不会一遍又一遍地创建新对象。我对 1 个对象使用相同的解决方案,我知道它必须永远存在,直到应用程序关闭。但是,如果每次客户端连接自己时都创建这样的对象,那么该解决方案将不起作用,因为它们永远不会被 GC 处理,并且您的内存消耗会不断增加,直到您停止服务器或由于没有更多内存而崩溃. 我有“Answer Checker”模块,它们会在源代码更改时动态编译和重新编译。我使用单独的应用程序域,以便可以卸载和重新加载模块。如果我有一百个问题,每个问题都有自己的模块,并且只为每个问题创建一次 MarshalByRef 对象,那么拥有一百个这样的对象会导致服务器耗尽资源吗?【参考方案8】:我最近也遇到了这个异常。现在我的解决方案只是卸载 AppDomain,然后在很长一段时间后重新加载 AppDomain。幸运的是,这个临时解决方案适用于我的情况。我希望有一种更优雅的方式来处理这个问题。
【讨论】:
【参考方案9】:不幸的是,当 AppDomains 用于插件目的时,此解决方案是错误的(不得将插件程序集加载到您的主应用程序域中)。
您的构造函数和析构函数中的GetRealObject() 调用会导致获取远程对象的真实类型,这会导致尝试将远程对象的程序集加载到当前的AppDomain 中。这可能会导致异常(如果无法加载程序集)或您加载了以后无法卸载的外部程序集的不良影响。
如果您使用 ClientSponsor.Register() 方法在主 AppDomain 中注册远程对象(不是静态的,因此您必须创建客户端赞助商实例),则可能是更好的解决方案。默认情况下,它将每 2 分钟更新一次远程代理,如果您的对象具有默认的 5 分钟生命周期,这就足够了。
【讨论】:
我添加了 base.TypeInfo.TypeName = typeof(CrossAppDomainObject).AssemblyQualifiedName;在 CrossAppDomainObjRef ctor 中,但在某些情况下它仍然失败,而且引用计数可能导致循环引用泄漏...... 我测试并确认了这一点。它不适用于插件机制。【参考方案10】:我终于找到了一种方法来执行客户端激活实例,但它涉及 Finalizer 中的托管代码 :( 我将我的课程专门用于 CrossAppDomain 通信,但您可以修改它并尝试其他远程处理。 如果您发现任何错误,请告诉我。
以下两个类必须位于所有相关应用程序域中加载的程序集中。
/// <summary>
/// Stores all relevant information required to generate a proxy in order to communicate with a remote object.
/// Disconnects the remote object (server) when finalized on local host (client).
/// </summary>
[Serializable]
[EditorBrowsable(EditorBrowsableState.Never)]
public sealed class CrossAppDomainObjRef : ObjRef
/// <summary>
/// Initializes a new instance of the CrossAppDomainObjRef class to
/// reference a specified CrossAppDomainObject of a specified System.Type.
/// </summary>
/// <param name="instance">The object that the new System.Runtime.Remoting.ObjRef instance will reference.</param>
/// <param name="requestedType"></param>
public CrossAppDomainObjRef(CrossAppDomainObject instance, Type requestedType)
: base(instance, requestedType)
//Proxy created locally (not remoted), the finalizer is meaningless.
GC.SuppressFinalize(this);
/// <summary>
/// Initializes a new instance of the System.Runtime.Remoting.ObjRef class from
/// serialized data.
/// </summary>
/// <param name="info">The object that holds the serialized object data.</param>
/// <param name="context">The contextual information about the source or destination of the exception.</param>
private CrossAppDomainObjRef(SerializationInfo info, StreamingContext context)
: base(info, context)
Debug.Assert(context.State == StreamingContextStates.CrossAppDomain);
Debug.Assert(IsFromThisProcess());
Debug.Assert(IsFromThisAppDomain() == false);
//Increment ref counter
CrossAppDomainObject remoteObject = (CrossAppDomainObject)GetRealObject(new StreamingContext(StreamingContextStates.CrossAppDomain));
remoteObject.AppDomainConnect();
/// <summary>
/// Disconnects the remote object.
/// </summary>
~CrossAppDomainObjRef()
Debug.Assert(IsFromThisProcess());
Debug.Assert(IsFromThisAppDomain() == false);
//Decrement ref counter
CrossAppDomainObject remoteObject = (CrossAppDomainObject)GetRealObject(new StreamingContext(StreamingContextStates.CrossAppDomain));
remoteObject.AppDomainDisconnect();
/// <summary>
/// Populates a specified System.Runtime.Serialization.SerializationInfo with
/// the data needed to serialize the current System.Runtime.Remoting.ObjRef instance.
/// </summary>
/// <param name="info">The System.Runtime.Serialization.SerializationInfo to populate with data.</param>
/// <param name="context">The contextual information about the source or destination of the serialization.</param>
public override void GetObjectData(SerializationInfo info, StreamingContext context)
Debug.Assert(context.State == StreamingContextStates.CrossAppDomain);
base.GetObjectData(info, context);
info.SetType(typeof(CrossAppDomainObjRef));
现在是 CrossAppDomainObject,您的远程对象必须继承自此类而不是 MarshalByRefObject。
/// <summary>
/// Enables access to objects across application domain boundaries.
/// Contrary to MarshalByRefObject, the lifetime is managed by the client.
/// </summary>
public abstract class CrossAppDomainObject : MarshalByRefObject
/// <summary>
/// Count of remote references to this object.
/// </summary>
[NonSerialized]
private int refCount;
/// <summary>
/// Creates an object that contains all the relevant information required to
/// generate a proxy used to communicate with a remote object.
/// </summary>
/// <param name="requestedType">The System.Type of the object that the new System.Runtime.Remoting.ObjRef will reference.</param>
/// <returns>Information required to generate a proxy.</returns>
[EditorBrowsable(EditorBrowsableState.Never)]
public sealed override ObjRef CreateObjRef(Type requestedType)
CrossAppDomainObjRef objRef = new CrossAppDomainObjRef(this, requestedType);
return objRef;
/// <summary>
/// Disables LifeTime service : object has an infinite life time until it's Disconnected.
/// </summary>
/// <returns>null.</returns>
[EditorBrowsable(EditorBrowsableState.Never)]
public sealed override object InitializeLifetimeService()
return null;
/// <summary>
/// Connect a proxy to the object.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
public void AppDomainConnect()
int value = Interlocked.Increment(ref refCount);
Debug.Assert(value > 0);
/// <summary>
/// Disconnects a proxy from the object.
/// When all proxy are disconnected, the object is disconnected from RemotingServices.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
public void AppDomainDisconnect()
Debug.Assert(refCount > 0);
if (Interlocked.Decrement(ref refCount) == 0)
RemotingServices.Disconnect(this);
【讨论】:
这是错误的。您应该使用父 AppDomain 中的 ISponsor 来管理子 AppDomain 中实例的生命周期。这就是 MBRO 的设计目的。这是一个受 COM 启发的 hack。 @Guillaume:它实际上很容易实现。您在父域中的代理上调用 InitializeLifetimeService。它返回一个您投射到 ILease 的对象。然后,您在通过 ISponsor 的租约上调用 Register。每隔一段时间,框架就会在 ISponsor 上调用 Renewal,您所要做的就是确定是否要更新代理并返回适当的 TimeSpan 长度。 @Guillaume:当你调用 CreateInstance(From)AndUnwrap 时你会这样做。那是您创建代理的时候,所以下一步是处理代理应该与另一个 AppDomain 中的实例保持连接的时间。 @Guillaume:嗯,你要做你该做的。搜索此答案的人了解正在发生的事情非常重要。 总是 从 MBRO.ILS 返回 null 就像总是捕获和吞下异常。是的,有时您应该这样做,但前提是您确切知道自己在做什么。 @Will:谢谢,我几乎从您的 cmets 中提取了一个解决方案。但是你为什么不给出一个完整、正确的答案呢?以上是关于AppDomain 和 MarshalByRefObject 生命周期:如何避免 RemotingException?的主要内容,如果未能解决你的问题,请参考以下文章
自定义 AppDomain 和 PrivateBinPath
默认 AppDomain 与新 AppDomain 中不同的依赖解析行为加载程序集