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

本文关键字:引用 方法 Do 保留 事件 回调 | 更新日期: 2023-09-27 18:36:12

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

我正在运行 ANT 内存分析器,我看到从 ConcurrentDictionary 属性的数百个实例中引用了 1000 个 MyObj 实例。 这些的 GC 根是事件回调。

这似乎导致内存在应用程序运行时显着增加。大约 5 分钟左右后,该内存的很大一部分被回收,但我担心该应用程序可能会遇到问题,因为它在 GC 启动之前会快速膨胀 sto。

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

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

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);
        }
    }
}
发布

数据方法发布棱镜事件

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

发生了什么

的,事件是委托列表,其中有两个相关字段: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; }
  }

这将帮助您找到为什么它被订阅了这么多次。

委托包含对对象的强引用以及对该对象调用的方法的指示。 因此,包含对委托的强引用的活动对象将使委托将在其上运行的对象(以及该对象具有强引用的任何对象)保持活动状态。

有时人们会希望注册一个回调或事件,该回调或事件将对对象进行操作,以造福其他对象,但回调或事件不应仅仅为了对象本身而使对象保持活动状态。 例如,对象可能希望记录某个长期对象引发特定事件的次数,从而创建它附加到事件的"事件计数器"对象的实例。 只要长期对象持有计数器对象的事件订阅,该计数器对象就会保持活动状态,并且每次引发事件时计数器都会递增。 当然,如果每个曾经看过计数器的人都不复存在,那么计数器和增加计数器所需的努力都不会有任何有用的目的。

如果有一个回调,

人们期望它将在将来的某个确定的点被触发,但回调只有在存在对它将对其进行操作的对象的某些实时引用(在回调本身之外)时才有用,那么将回调注册到转发对象可能是有用的,如果这样做有意义,转发对象会反过来将调用转发到主对象。 实现此目的的最简单方法是让转发对象保存一个WeakReference。 当转发对象收到调用时,它会从WeakReference中检索Target。 如果这是非 null,它将检索到的目标强制转换为主对象的类型,并对其调用适当的方法。 如果主对象在回调执行之前不复存在,则 WeakReference.Target 属性将为 null,转发对象的回调将仅静默返回。

进一步的注意事项:(1)将WeakReferenceTarget设置为委托并调用它可能很诱人,但这种方法只有在真正的目标对象本身包含对该委托的引用时才有效; 否则,即使委托本身的目标不是,它也有资格进行垃圾回收;(2) 将WeakReference转换为接口并让主对象实现该接口可能会有所帮助。 这将允许一个转发对象类与许多其他类一起使用。 如果一个类可能希望附加到许多弱事件,则使用泛型接口可能会有所帮助:

接口IDispatchAction<DummyType,ParamType>{  void Act(paramType ref param);}

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

引发外部事件一次后,取消订阅你的类(SomeClass.SomeEvent -= MyEventHandler),或者你可以使用 WeakReferences 查看

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

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收集后,多余的对象消失了(换句话说,确保它们没有被泄漏),你就会没事的。

如果我们希望对象定义的回调方法不应该留在内存中,我们必须将此方法定义为 Static。