你如何安全地枚举一个 List<Weakreference> 而没有终结器妨碍?

How do you safely enumerate a List<Weakreference> without finalizers getting in the way?

我的应用程序中有一个 WeakReference 的静态列表。在某个时候,我想拍摄此列表中所有当前 "alive" 对象的快照。

代码是这样的:

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 循环中遇到 "Collection Was Modified" 异常。我只从 MyObject constructor/finalizers 中的这个列表中 add/remove,它看起来像这样:

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?

是否有 better/correct/safer 枚举弱引用列表的方法?

这可能会损害 GC and/or 终结器(性能下降),因为您在快速执行场景中使用 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<T>> 替换为 WeakCollection<T>

WeakCollection<T> 是集合,不是列表。永远不要在集合就足够的地方使用列表。

WeakCollection<T> 完全封装了它包含弱引用的事实,这意味着您将它用作常规列表,其接口中没有任何关于弱引用的内容。

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

这是 WeakCollection<T> 的示例实现。当心:我没有测试过。

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