您如何安全地枚举 List<Weakreference> 而没有终结器的阻碍?

Posted

技术标签:

【中文标题】您如何安全地枚举 List<Weakreference> 而没有终结器的阻碍?【英文标题】:How do you safely enumerate a List<Weakreference> without finalizers getting in the way? 【发布时间】:2019-12-12 18:22:34 【问题描述】:

我的应用程序中有一个 WeakReference 的静态列表。在某个时候,我想为这个列表中所有当前“活动”的对象拍张快照。

代码是这样的:

private static readonly List<WeakReference> myObjects = new List<WeakReference>();

public static MyObject[] CollectObjects()

    var list = new List<MyObject>();
    foreach (var item in myObjects)
    
        if (!item.IsAlive)
            continue;
        var obj = item.Target as MyObject;
        list.Add(obj);
    
    return list.ToArray();

我遇到的问题是我有时(很少)在上面的 foreach 循环中遇到“集合已修改”异常。我只在 MyObject 构造函数/终结器中从这个列表中添加/删除,如下所示:

public class MyObject

    private static readonly object _lockObject = new object();
    WeakReference referenceToThis;
    public MyObject()
    
        lock (_lockObject)
        
            referenceToThis = new WeakReference(this);
            myObjects.Add(referenceToThis);
        
    
    ~MyObject()
    
        lock (_lockObject)
        
            myObjects.Remove(referenceToThis);
        
    

由于我的代码中没有任何其他内容涉及列表,因此我的假设是垃圾收集器正在完成其中一些对象,就像我尝试枚举列表一样。

我想过在 foreach 循环周围添加一个lock (_lockObject),但我不确定这样的锁会如何影响 GC?

是否有更好/正确/更安全的方法来枚举 WeakReferences 列表?

【问题讨论】:

您是否尝试将true 作为第二个参数传递给WeakReference ctor?还有一个现有的thred 可能会有所帮助 不,我不得不承认我不明白这个参数的作用。 ~MyObject() 在 C# 中,你不知道什么时间调用那个,如果myObjects refrerence 你的对象,它不能为 GC 释放,所以可能~MyObject() 从来没有工作? 不锁定 foreach 循环是一个简单的错误,您必须修复它。请注意,急于销毁 WeakReference 通常是不明智的。例如,您最好在迭代集合时执行此操作,尽管您随后需要 for 而不是 foreach。 锁定插入和删除而不是围绕 foreach 显然对您的问题没有影响。但是我不确定在这里使用 lock 是否是个好主意:如果您手动检查“活动”,为什么还要自动从列表中删除死对象? 【参考方案1】:

终结器在整个垃圾收集机制中引入了很大的开销,因此最好尽可能避免它。在您的情况下,您可以避免它并大大简化您的设计。

不要让您的对象通过终结器检测自己的终结并从该列表中删除自己,而是将您的 List&lt;WeakReference&lt;T&gt;&gt; 替换为 WeakCollection&lt;T&gt;

WeakCollection&lt;T&gt; 是一个集合,而不是一个列表。永远不要在集合就足够的地方使用列表。

WeakCollection&lt;T&gt; 完全封装了它包含弱引用的事实,这意味着您将它用作常规列表,它的接口中没有弱引用。

WeakCollection&lt;T&gt; 在检测到弱引用已过期时会自动将其从自身中删除。 (您会发现将其实现为列表要比将其实现为集合要复杂得多,所以再次强调一下——永远不要在集合就足够的地方使用列表。)

这是WeakCollection&lt;T&gt; 的示例实现。注意:我没有测试过。

namespace Whatever

    using Sys = System;
    using Legacy = System.Collections;
    using Collections = System.Collections.Generic;

    public class WeakCollection<T> : Collections.ICollection<T> where T : class
    
        private readonly Collections.List<Sys.WeakReference<T>> list = new Collections.List<Sys.WeakReference<T>>();

        public void                           Add( T item )   => list.Add( new Sys.WeakReference<T>( item ) );
        public void                           Clear()         => list.Clear();
        public int                            Count           => list.Count;
        public bool                           IsReadOnly      => false;
        Legacy.IEnumerator Legacy.IEnumerable.GetEnumerator() => GetEnumerator();

        public bool Contains( T item )
        
            foreach( var element in this )
                if( Equals( element, item ) )
                    return true;
            return false;
        

        public void CopyTo( T[] array, int arrayIndex )
        
            foreach( var element in this )
                array[arrayIndex++] = element;
        

        public bool Remove( T item )
        
            for( int i = 0; i < list.Count; i++ )
            
                if( !list[i].TryGetTarget( out T target ) )
                    continue;
                if( Equals( target, item ) )
                
                    list.RemoveAt( i );
                    return true;
                
            
            return false;
        

        public Collections.IEnumerator<T> GetEnumerator()
        
            for( int i = list.Count - 1; i >= 0; i-- )
            
                if( !list[i].TryGetTarget( out T element ) )
                
                    list.RemoveAt( i );
                    continue;
                
                yield return element;
            
        
    

【讨论】:

有趣,不知道将维护逻辑放在 GetEnumerator 中并让 GetEnumerator 以相反的顺序返回列表的感觉对我来说有点奇怪。 我再说一遍:别再把它当成一个列表了。这是一个集合。因此,它是无序的。因此,物品可以按任何顺序退回。但是如果你实在受不了这个,你可以改变它,只是会稍微复杂一些。至于枚举器中的维护逻辑,嗯,缓存就是这样工作的,这本质上就是一个缓存。 我明白你的意思了。 我认为这是一个更好的方法,再次感谢分享!【参考方案2】:

这可能会损害 GC 和/或终结器(影响性能),因为您在快速执行的场景中使用了 lock(关键部分)机制。

您确实应该将枚举包装在同步块中,但您还应该实现 SpinLock (how to) 模式(而不是 lock),这对于快速同步块非常有用:

readonly SpinLock sl = new SpinLock();
bool lockTaken = false;

...

try

    sl.Enter(ref lockTaken);
    // do your thing

finally

    if (lockTaken)
    
        sl.Exit();
    

【讨论】:

以上是关于您如何安全地枚举 List<Weakreference> 而没有终结器的阻碍?的主要内容,如果未能解决你的问题,请参考以下文章

从底层类型安全地转换枚举类

Java面试经典合集1:如何安全地删除List中的元素

IAsyncEnumerator.Current 在未将枚举数集合强制转换为 List 时返回 null

如何将 List<string> 分配给 JavaScript 数组或可枚举对象

面试官:如何安全地使用List

怎样线程安全地遍历List:VectorCopyOnWriteArrayList