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

63
当从一个应用程序域(1)传递MarshalByRef对象到另一个应用程序域(2)时,如果在第二个应用程序域(2)上调用该对象的方法之前等待6分钟,则会出现RemotingException:

  

System.Runtime.Remoting.RemotingException:   对象[...]已断开连接或   在服务器上不存在。

关于此问题的一些文档:

如果InitializeLifetimeService返回null,那么即使代理被收集,该对象也只能在AppDomain 2卸载时才能在AppDomain 1中收集吗?

有没有办法禁用生存期并保持代理(在AppDomain 2中)和对象(在AppDomain1中)保持活动状态,直到代理被Finalized?也许可以使用ISponsor...?

10个回答

47

实际上,我只有一个远程对象。操作可能需要非常长的时间才能完成(根据用户数据,可能需要几天时间...)。使用这种实现方式,不会耗尽资源-我已经进行了测试和反复测试。 - scrat.squirrel
9
有几个人在没有解释的情况下给这个帖子投了反对票。虽然这可能意味着什么都没有,但从文明的角度来看,知道为什么会很好。此外,这种解决方案在现实商业应用中非常有效,我并不是凭空想出来的。 - scrat.squirrel
8
我猜测你被点踩的原因是你的解决方案相当极端。当然,在你的实际商业应用中,它起作用是因为你不断地创建新对象。我使用同样的解决方案针对我知道需要一直存在直到应用关闭的1个对象。但如果每次客户端连接时都创建这样一个对象,那么该解决方案就行不通了,因为它们永远不会被垃圾回收,你的内存消耗将不断增加,最终要么停止服务器,要么因为没有更多的可用内存而崩溃。 - user276648
我有“答案检查器”模块,当源代码更改时会动态编译和重新编译。我使用单独的应用程序域,以便可以卸载和重新加载模块。如果我有一百个问题,每个问题都有自己的模块,并为它们中的每一个创建一个MarshalByRef对象仅一次,那么拥有一百个这样的对象会导致服务器耗尽资源吗? - Triynko
@Triynko,您应该创建一个新问题,链接到这个答案。 - Guillaume
显示剩余2条评论

13

我终于找到一种实现客户端激活实例的方法,但它需要在 Finalizer 中使用托管代码 :( 我为跨应用程序域通信专门设计了我的类,但您可以修改它并尝试在其他远程过程调用中使用。 如果您发现任何错误,请告诉我。

以下两个类必须在涉及到的所有应用程序域中加载的程序集中。

  /// <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);
    }
  }

3
这是错误的。你应该使用来自父应用程序域的ISponsor来管理子应用程序域中实例的生命周期,这就是MBRO设计的目的。使用这种方式相当于是一种受COM启发的欺骗手段。 - user1228
2
@Guillaume:实际上实现起来非常容易。您在父域中调用代理的InitializeLifetimeService方法。它会返回一个对象,您将其转换为ILease接口。然后,您调用租约上的Register方法,并传入一个ISponsor接口。每隔一段时间,框架都会调用ISponsor接口上的Renewal方法,您只需要确定是否要续订代理并返回适当的TimeSpan长度即可。 - user1228
1
@Guillaume:当你调用CreateInstance(From)AndUnwrap时才会发生这件事。这时候你创建了代理,所以下一步是处理代理应该与其他AppDomain中的实例保持连接的时间。 - user1228
3
@Guillaume:嗯,你得做你该做的事情。重要的是搜索这个答案的人要理解正在发生的事情。始终从MBRO.ILS返回null就像总是捕获和吞噬Exception一样。是的,有时候你应该这样做,但只有当你确切知道自己在做什么时才可以这样做。 - user1228
2
@Will:谢谢,我差点从你的评论中找到了解决方案。但是为什么你不给出一个完整、正确的答案呢? - ali_bahoo
显示剩余6条评论

9

这里有两种可能的解决方案。

单例模式:覆盖InitializeLifetimeService

正如原帖中链接的Sacha Goldshtein在博客文章中指出的那样,如果您的远程对象具有单例语义,您可以覆盖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在另一个答案中指出的那样,

如果每次客户端连接时都创建这样一个对象,那么这种解决方案将无法工作,因为它们永远不会被垃圾回收,你的内存消耗将不断增加,直到你停止服务器或它因没有更多内存而崩溃

基于类的方法:使用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中的生存期管理是如何工作的,这在MSDN上有详细描述。我引用了我认为最有用的部分:

远程生存期服务将租约与每个服务关联,并在其租约到期时删除该服务。生存期服务可以承担传统分布式垃圾回收器的功能,并且当服务器每个客户端的数量增加时,它也能很好地进行调整。

每个应用程序域都包含一个租约管理器,负责控制其域中的租约。所有租约定期检查是否存在过期的租约时间。如果租约已经过期,则会调用一个或多个租约的赞助商,并给予它们更新租约的机会。如果没有赞助商决定更新租约,则租约管理器会删除租约,对象可以被垃圾回收器收集。租约管理器维护一个按剩余租约时间排序的租约列表。剩余时间最短的租约存储在列表的顶部。远程生存期服务将租约与每个服务关联,并在其租约到期时删除该服务。


这个答案被低估了。 - Gerben Limburg
微软推出了ClientSponsor类来替换原有的sponsporshipManager。这个未公开的问题是,_赞助商也有一个租约_,所以当它过期时,它就无法响应续约请求。 ClientSponsor使用非到期租约创建自己,因此它会像预期那样一直更新赞助对象。同样未公开的是,ClientSponsor可以注册多个对象。 - Suncat2000

7

很遗憾,当AppDomains用于插件目的时,此解决方案是错误的(插件的程序集不得加载到主应用程序域中)。

在您的构造函数和析构函数中调用GetRealObject()会导致获取远程对象的真实类型,这将尝试将远程对象的程序集加载到当前AppDomain中。这可能会导致异常(如果无法加载程序集)或者产生不需要的效果,即您已经加载了一个外部程序集,但以后不能卸载。

更好的解决方案是使用ClientSponsor.Register()方法将远程对象注册到主应用程序域中(该方法不是静态的,因此必须创建客户端赞助人实例)。默认情况下,它将在每2分钟内更新您的远程代理,如果您的对象具有默认的5分钟寿命,则足够长。


我在CrossAppDomainObjRef构造函数中添加了base.TypeInfo.TypeName = typeof(CrossAppDomainObject).AssemblyQualifiedName;,但在某些情况下仍然失败,此外,引用计数可能导致循环引用的泄漏... - Guillaume
我进行了测试并确认,它不适用于插件机制。 - Elo

2

是的,我们已经阅读了。它没有关于为什么赞助商没有被召唤续签租约的信息。 - Suncat2000

1
[SecurityPermissionAttribute(SecurityAction.Demand, Flags = SecurityPermissionFlag.Infrastructure)]
public override object InitializeLifetimeService()
{
  return null;
}

我已经测试过它,它可以正常工作,当然需要知道代理会一直存在,直到自己进行垃圾回收。但在我的情况下,使用连接到主应用程序的插件工厂,没有内存泄漏或类似的问题。我只是确保我实现了IDisposable,它可以正常工作(我可以说,因为我的加载的dll(在工厂中)可以在正确处理工厂后被覆盖)。 编辑:如果您通过域传递事件,请在创建代理的类中添加此行代码,否则您的冒泡也会报错;)

1

我创建了一个在销毁时断开连接的类。

public class MarshalByRefObjectPermanent : MarshalByRefObject
{
    public override object InitializeLifetimeService()
    {
        return null;
    }

    ~MarshalByRefObjectPermanent()
    {
        RemotingServices.Disconnect(this);
    }
}

1
如果您想在远程对象被垃圾回收后重新创建它,而无需创建ISponsor类或给它无限生命周期,则可以在捕获RemotingException的同时调用远程对象的虚拟函数。请保留HTML标签。
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);
    }
}

呃,这是一个相当不太优雅的解决方案。 - Elo

0
您可以尝试使用一个序列化的单例 ISponsor 对象实现 IObjectReference。GetRealObject 实现(从 IObjectReference)应该在 context.State 为 CrossAppDomain 时返回 MySponsor.Instance,否则返回自身。MySponsor.Instance 是一个自初始化、同步的 (MethodImplOptions.Synchronized) 单例。Renewal 实现(来自 ISponsor)应该检查静态变量 MySponsor.IsFlaggedForUnload,并在被标记为卸载/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 卸载为止。

-2

我最近也遇到了这个异常。目前我的解决方案是卸载 AppDomain,然后在长时间间隔后重新加载 AppDomain。幸运的是,这个临时解决方案适用于我的情况。我希望有一种更优雅的方式来处理这个问题。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接