繁体   English   中英

在C#中,事件是否保留对回调方法所在的整个类的引用?

[英]In C#, Do events keep a reference to the entire class where the call back method is located?

我有一个将ConcurrentDictionary作为私有成员的类。 此类还定义了一个委托/回调方法。 基类将此方法注册为外部事件的回调。 这是唯一的一次。

我正在运行ANT内存探查器,并且从数百个ConcurrentDictionary属性实例中引用了数千个MyObj实例。 这些的GC根是事件回调。

这似乎导致内存随着应用程序的运行而显着增加。.大约5分钟后,该内存的很大一部分将被回收,但是我担心该应用程序可能会出现问题,因为它会迅速膨胀并花了很长时间才开始GC

这是怎么回事,我该如何解决?

这是注册处理程序的基本调用的片段

protected abstract void DataReceivedEventHandler(DataChangedEvent evt);

public virtual void RegisterForChanges(ICollection<MemoryTable> tables)
{
    foreach (MemoryTable table in tables)
    {
        _subscribedTables.Add(table);
        table.RegisterEventListener(new DataChangedCallBack(this.DataReceivedEventHandler));

    }
}

这是在上述基类的子类中实现的处理程序:

private ConcurrentDictionary<string, DataRecord> _cachedRecords;

protected override void DataReceivedEventHandler(DataChangedEvent evt)
{
    DataRecord record = evt.Record as DataRecord;
    string key = record.Key;
    if (string.IsNullOrEmpty(key)) { return; }

    if (_cachedRecords.ContainsKey(key))
    {
        _cachedRecords[key] = record;

        DateTime updateTime = record.UpdateTime;
        TimeSpan delta = updateTime - _lastNotifyTime;
        if (delta.TotalMilliseconds > _notificationFrequency)
        {
            PublishData(updateTime);
        }
    }
}

publishData方法发布棱镜事件

这是怎么回事

是的,事件是代表的列表,它具有两个相关的字段: targetmethod 除非您引用静态方法,否则target是对该类的引用。 method是反映MemberInfo ,它告诉事件要调用的方法。

如何排除故障

考虑在add_EventName方法中放置一个断点。 (如果没有显式的add_EventName和remove_EventName,则必须使用此显式代码重新定义事件)。

  private event EventHandler eventName;
  public event EventHandler EventName
  {
     add { eventName += value; } // Breakpoint here
     remove { eventName -= value; }
  }

这将帮助您找到为什么订阅了这么多次的原因。

一次引发外部事件后,请从该类退订您的类(SomeClass.SomeEvent-= MyEventHandler),或者您可以使用WeakReferences查看

委托包含对对象的强引用以及对该对象调用哪种方法的指示。 拥有对委托的强引用的活动对象将使委托将在其上进行操作的对象(以及该对象对其具有强引用的任何对象)保持活动状态。

有时,人们希望注册一个回调或事件,该回调或事件将对某个对象进行操作以利于其他对象,但是回调或事件不应仅出于自身目的而使该对象保持活动状态。 例如,一个对象可能希望保留某个长期存在的对象引发特定事件的次数,从而创建一个附加到该事件的“事件计数器”对象的实例。 只要寿命很长的对象为计数器对象保留事件订阅,该计数器对象将保持活动状态,并且每次引发事件时计数器都会递增。 当然,如果每个曾经看过柜台的人都不复存在,那么无论是柜台还是增加柜台所需的努力都不会达到任何有用的目的。

如果有人希望在将来某个确定的时间触发回调,但只有在对其进行操作的对象存在一些实时引用(在回调本身之外)时,该回调才有用处。将回调注册到转发对象很有用,如果这样做有意义,它将把调用转发给主对象。 最简单的方法是使转发对象持有WeakReference 当转发对象收到呼叫时,它将从WeakReference检索Target 如果非零,则将检索到的目标强制转换为主对象的类型,并在其上调用适当的方法。 如果在执行回调之前主要对象不再存在,则WeakReference.Target属性将为null,转发对象的回调将仅以静默方式返回。

还有两点需要注意:(1)将WeakReferenceTarget设置为委托并对其进行调用可能很诱人,但是该方法仅在实际目标对象本身持有对该委托的引用时才有效; 否则,委托人本身将有资格进行垃圾回收,即使其目标不是目标也是如此。 (2)将WeakReference转换为接口并让主要对象实现该接口可能会有所帮助。 这将允许一个转发对象类与许多其他类一起使用。 如果一个类可能希望与许多弱事件相关联,则使用通用接口可能会有所帮助:

interface IDispatchAction<DummyType,ParamType>
{
  void Act(ParamType ref param);
}

这将允许主对象公开许多IDispatchAction操作(例如,如果一个类实现了IDispatchAction<foo,int>.ActIDispatchAction<bar,int>.Act ,则将对该类的引用转换为这些接口之一)并对其调用Act将调用相应的方法)。

您是否可能一遍又一遍地重新预订表? 我看到这个:

foreach (MemoryTable table in tables)
{
    _subscribedTables.Add(table);
    table.RegisterEventListener(new DataChangedCallBack(this.DataReceivedEventHandler));
}

并且我希望看到检查以确保没有重新预订表:

foreach (MemoryTable table in tables)
{
    if (!_subscribedTables.Contains(table)) {
        _subscribedTables.Add(table);
        table.RegisterEventListener(new DataChangedCallBack(this.DataReceivedEventHandler));
    }
}

编辑:考虑到问题开头的评论,我相当有信心这个问题(如果可以称之为问题)就在这里:

if (_cachedRecords.ContainsKey(key))
{
    _cachedRecords[key] = record;

您在这里所说的是,如果该记录的键已经存在于cachedRecords ,则将该值替换为(可能是)新的行实例。 这可能是因为某些后台进程导致了行数据的更改,并且您需要将这些新值传播到UI。

我的猜测是MemoryTable类正在为这些更改创建DataRecord的新实例 ,并将该新实例沿事件链发送到我们在此处看到的处理程序。 如果事件被触发了数千次,那么您最终将在内存中留下数千个事件。 垃圾收集器通常很好地清理这些内容,但是您可能需要考虑就地更新,以避免在收集这些实例时发生大规模GC。

应该做的是尽量控制(或者甚至预测)时,GC会运行。 只要确保在GC收集完之后,多余的对象就消失了(换句话说,请确保它们没有泄漏),您就可以了。

如果希望对象定义的回调方法不保留在内存中,则必须将此方法定义为“静态”。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM