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 加载/卸载

自定义 AppDomain 和 PrivateBinPath

默认 AppDomain 与新 AppDomain 中不同的依赖解析行为加载程序集

CLR解析AppDomain

AppDomain 和 MarshalByRefObject 生命周期:如何避免 RemotingException?

AppDomain 间通信和接口