【问题标题】:Good implementation of weak dictionary in .Net.Net中弱字典的良好实现
【发布时间】:2011-02-16 13:51:32
【问题描述】:

我在哪里可以找到内部使用弱引用的 IDictionary 的良好实现?

字典应该只保存对值的弱引用,并最终清除自己的死引用。

还是我自己写?

【问题讨论】:

标签: .net dictionary weak-references


【解决方案1】:

ConditionalWeakTable Class 使用弱键并在表外不存在对键的其他引用时自动删除键/值条目。

【讨论】:

  • 需要注意的是,这个类使用ReferenceEquals而不是GetHashCodeEquals来做相等检查。请参阅stackoverflow.com/a/8441180/167251 进行更深入的讨论。
  • OP 要求使用弱的弱字典。 .NET ConditionalWeakTable 类是具有弱 的弱字典。因此,我不认为这是一个正确的答案。
  • 但这正是我想要的:P 所以感谢您确认!
【解决方案2】:

您需要自己编写。它应该相对简单,实现IDictionary<T,T> 接口,然后将实际值存储为WeakReferences<T>。然后,您可以使用 TryGetTarget 检查添加/选择上的值,看看它们是否仍然存在。

public class WeakDictionary <TKey,TValue> : IDictionary<TKey,TValue>
    where TValue : class
{
    private readonly Dictionary<TKey,WeakReference<TValue>> innerDictionary = new Dictionary<TKey,WeakReference<TValue>>();
    
    
    public TValue Index[ TKey key ]
    {
        get
        {
            // Use .TryGetTarget instead of .IsAlive and .Target
            if (this.innerDictionary.TryGetValue(key, out WeakReference<TValue> wf) && wf.TryGetTarget(out TValue value))
            {
                return value;
            }

            return null;
        }
        
    }
    
    private void Cull()
    {
        var deadKeys = this.innerDictionary.Where(kvp => kvp.Value.IsAlive).Select(kvp => kvp.Key).ToList();

        foreach (var key in deadKeys)
        {
            _ = this.innerDictionary.TryRemove(key);
        }
    }
}

【讨论】:

  • 请注意垃圾收集可能在reference.IsAlivereference.Target 之间发生,从而使该解决方案容易出现竞争条件。
  • 在检查 IsAlive 修复该竞争之前,将 Target 放在您自己的堆栈上很容易
  • 你为什么不使用 Wea​​kReference.TryGetValue?
  • 是的,就像其他人建议的那样,使用 TryGetTarget 和 WeakReference
【解决方案3】:

简单地持有一个 WeakReference 对象字典的一个问题是,除了枚举整个字典之外,没有办法从字典中删除其目标超出范围的任何 WeakReference 对象。

如果 WeakReference 可以包含一个委托,该委托将在主要目标超出范围时被调用,这将很有帮助。据我所知,没有办法做到这一点。如果您不介意为您存储在“弱字典”中的对象添加另一个字段和一些代码,我建议创建我称之为“Finaposer”对象,其唯一字段是 MethodInvoker;处置时, MethodInvoker 应该被清空;终结器应该 Interlocked.Exchange() 将 MethodInvoker 设置为 null 并且——如果它的旧值不为 null——调用它。要写入字典的对象应创建一个新的 Finasposer 对象,并带有一个委托,该委托将在方便时将键从字典中删除。

请注意,终结器和由此调用的任何委托都不应该直接操作字典,也不应该做任何需要获取锁的事情。如果 Finasposer 持有一个委托,则该委托本身在 Finalize 执行时保证是有效的,但附加到该委托的对象以及由此引用的任何对象可能处于意外状态。但是,对于 Finasposer 调用的方法来说,向链表添加对超出范围的对象的引用应该是安全的。 Dictionary 的 Add、Remove 和其他方法可以轮询链表以查看其中的任何 WeakReference 是否已死亡并需要清除。

【讨论】:

  • 有办法在对象被收集后有一个通知。 ConditionalWeakTable 可以在这里提供帮助。详情请看我的博客WeakTable
  • 我只想说,一看到这个答案第二段的前两句,我就知道它是超级猫。 (在我向下滚动确认之前。)签名风格和语气。与我 12 年来阅读的本网站上的所有其他回答者相比,它脱颖而出。周到且尤其具有探索性。我从来没有不喜欢超级猫的答案。我不想让这成为粉丝的事情,我只是非常欣赏答案风格。
  • @supercat - 请参阅下面的答案。我想我正在做你说不要做的事情。我可以理解不从 Finalizer 线程做事——比如操作字典和获取锁,但就你的其他建议而言,我看不出这些的原因。比如,使用 MethodInvoker 有什么特别之处?谢谢。
  • @N73k:我不认为ConditionalWeakTable 在我写答案时可用。至于Finasposer 类,其基本思想是将终结逻辑封装在一个通用类中,该类将在特定对象不再存在时运行指定的代码段。请注意,Finalize 方法实际上并不指示对象何时被垃圾回收,而是指示在对象树中某处没有注册的Finalize 方法的情况下对象本应被垃圾回收的时间。找出一个对象实际上何时被垃圾回收并...
  • ...可以保证永远不会“复活”[可以更准确地描述为“缓刑”]有点棘手;使用 Finasposer 类可以将程序的其余部分与此类棘手问题隔离开来。
【解决方案4】:

这将在没有其他解决方案的性能问题的情况下工作。

(它不依赖于在每次请求时调用“shrink”方法来手动删除死对象。这些“shrink”方法必须在每次调用时遍历每个项目。我确实有一个“收缩”方法,但只有在枚举项目时才会调用它。)

问题的关键是在 ConditionalWeakTable 中使用“holder”对象作为值,这样当 key 被删除时,holder 的 finalizer 将触发,从 key 的“活动列表”中删除 key。

我对此进行了测试,它可以工作。

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace Util
{
    public class WeakDictionary<TKey, TValue> : IDictionary<TKey, TValue>, IDisposable
        where TKey : class
        where TValue : class
    {
        private readonly object locker = new object();
        //private readonly HashSet<WeakReference> weakKeySet = new HashSet<WeakReference>(new ObjectReferenceEqualityComparer<WeakReference>());
        private ConditionalWeakTable<TKey, WeakKeyHolder> keyHolderMap = new ConditionalWeakTable<TKey, WeakKeyHolder>();
        private Dictionary<WeakReference, TValue> valueMap = new Dictionary<WeakReference, TValue>(new ObjectReferenceEqualityComparer<WeakReference>());


        private class WeakKeyHolder
        {
            private WeakDictionary<TKey, TValue> outer;
            private WeakReference keyRef;

            public WeakKeyHolder(WeakDictionary<TKey, TValue> outer, TKey key)
            {
                this.outer = outer;
                this.WeakRef = new WeakReference(key);
            }

            public WeakReference WeakRef { get; private set; }

            ~WeakKeyHolder()
            {
                this.outer?.onKeyDrop(this.WeakRef);  // Nullable operator used just in case this.outer gets set to null by GC before this finalizer runs. But I haven't had this happen.
            }
        }

        private void onKeyDrop(WeakReference weakKeyRef)
        {
            lock(this.locker)
            {
                if (!this.bAlive)
                    return;

                //this.weakKeySet.Remove(weakKeyRef);
                this.valueMap.Remove(weakKeyRef);
            }
        }

        
    // The reason for this is in case (for some reason which I have never seen) the finalizer trigger doesn't work
    // There is not much performance penalty with this, since this is only called in cases when we would be enumerating the inner collections anyway.
        private void manualShrink()
        {
            var keysToRemove = this.valueMap.Keys.Where(k => !k.IsAlive).ToList();

            foreach (var key in keysToRemove)
                valueMap.Remove(key);
        }

        private Dictionary<TKey, TValue> currentDictionary
        {
            get
            {
                lock(this.locker)
                {
                    this.manualShrink();
                    return this.valueMap.ToDictionary(p => (TKey) p.Key.Target, p => p.Value);
                }
            }
        }

        public TValue this[TKey key]
        {
            get
            {
                if (this.TryGetValue(key, out var val))
                    return val;

                throw new KeyNotFoundException();
            }

            set
            {
                this.set(key, value, isUpdateOkay: true);
            }
        }

        private bool set(TKey key, TValue val, bool isUpdateOkay)
        {
            lock (this.locker)
            {
                if (this.keyHolderMap.TryGetValue(key, out var weakKeyHolder))
                {
                    if (!isUpdateOkay)
                        return false;

                    this.valueMap[weakKeyHolder.WeakRef] = val;
                    return true;
                }

                weakKeyHolder = new WeakKeyHolder(this, key);
                this.keyHolderMap.Add(key, weakKeyHolder);
                //this.weakKeySet.Add(weakKeyHolder.WeakRef);
                this.valueMap.Add(weakKeyHolder.WeakRef, val);

                return true;
            }
        }

        public ICollection<TKey> Keys
        {
            get
            {
                lock(this.locker)
                {
                    this.manualShrink();
                    return this.valueMap.Keys.Select(k => (TKey) k.Target).ToList();
                }
            }
        }

        public ICollection<TValue> Values
        {
            get
            {
                lock (this.locker)
                {
                    this.manualShrink();
                    return this.valueMap.Select(p => p.Value).ToList();
                }
            }
        }

        public int Count
        {
            get
            {
                lock (this.locker)
                {
                    this.manualShrink();
                    return this.valueMap.Count;
                }
            }
        }

        public bool IsReadOnly => false;

        public void Add(TKey key, TValue value)
        {
            if (!this.set(key, value, isUpdateOkay: false))
                throw new ArgumentException("Key already exists");
        }

        public void Add(KeyValuePair<TKey, TValue> item)
        {
            this.Add(item.Key, item.Value);
        }

        public void Clear()
        {
            lock(this.locker)
            {
                this.keyHolderMap = new ConditionalWeakTable<TKey, WeakKeyHolder>();
                this.valueMap.Clear();
            }
        }

        public bool Contains(KeyValuePair<TKey, TValue> item)
        {
            WeakKeyHolder weakKeyHolder = null;
            object curVal = null;

            lock (this.locker)
            {
                if (!this.keyHolderMap.TryGetValue(item.Key, out weakKeyHolder))
                    return false;

                curVal = weakKeyHolder.WeakRef.Target;
            }

            return (curVal?.Equals(item.Value) == true);
        }

        public bool ContainsKey(TKey key)
        {
            lock (this.locker)
            {
                return this.keyHolderMap.TryGetValue(key, out var weakKeyHolder);
            }
        }

        public void CopyTo(KeyValuePair<TKey, TValue>[] array, int arrayIndex)
        {
            ((IDictionary<TKey, TValue>) this.currentDictionary).CopyTo(array, arrayIndex);
        }

        public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator()
        {
            return this.currentDictionary.GetEnumerator();
        }

        public bool Remove(TKey key)
        {
            lock (this.locker)
            {
                if (!this.keyHolderMap.TryGetValue(key, out var weakKeyHolder))
                    return false;

                this.keyHolderMap.Remove(key);
                this.valueMap.Remove(weakKeyHolder.WeakRef);

                return true;
            }
        }

        public bool Remove(KeyValuePair<TKey, TValue> item)
        {
            lock (this.locker)
            {
                if (!this.keyHolderMap.TryGetValue(item.Key, out var weakKeyHolder))
                    return false;

                if (weakKeyHolder.WeakRef.Target?.Equals(item.Value) != true)
                    return false;

                this.keyHolderMap.Remove(item.Key);
                this.valueMap.Remove(weakKeyHolder.WeakRef);

                return true;
            }
        }

        public bool TryGetValue(TKey key, out TValue value)
        {
            lock (this.locker)
            {
                if (!this.keyHolderMap.TryGetValue(key, out var weakKeyHolder))
                {
                    value = default(TValue);
                    return false;
                }
                
                value = this.valueMap[weakKeyHolder.WeakRef];
                return true;
            }
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return this.GetEnumerator();
        }

        private bool bAlive = true;

        public void Dispose()
        {
            this.Dispose(true);
        }

        protected void Dispose(bool bManual)
        {
            if (bManual)
            {
                Monitor.Enter(this.locker);

                if (!this.bAlive)
                    return;
            }
            
            try
            {
                this.keyHolderMap = null;
                this.valueMap = null;
                this.bAlive = false;
            }
            finally
            {
                if (bManual)
                    Monitor.Exit(this.locker);
            }
        }

        ~WeakDictionary()
        {
            this.Dispose(false);
        }
    }


public class ObjectReferenceEqualityComparer<T> : IEqualityComparer<T>
{
    public static ObjectReferenceEqualityComparer<T> Default = new ObjectReferenceEqualityComparer<T>();

    public bool Equals(T x, T y)
    {
        return ReferenceEquals(x, y);
    }

    public int GetHashCode(T obj)
    {
        return RuntimeHelpers.GetHashCode(obj);
    }
}

public class ObjectReferenceEqualityComparer : ObjectReferenceEqualityComparer<object>
{
}

}

【讨论】:

  • 很惊讶这没有更多的赞成票!非常聪明。只有一个缺点是键必须是类。
【解决方案5】:

对值有弱引用是一回事,但我发现字典键也可能是内存泄漏的来源。这是一个对键的弱引用的基本实现:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Common.library.collections {

    /// <summary>
    /// THIS DICTIONARY WILL NOT "HANG ON" TO THE KEYS IT USES
    /// IF THE KEY IS GARBAGE COLLECTED, THE VALUE WILL BE RELEASED TOO
    /// </summary>
    public class Dictionary_usingWeakKey<K, V> {
        //MAP FROM HASH CODE TO LIST OF KEY/VALUE PAIRS
        private Dictionary<int, List<Pair>> dic = new Dictionary<int, List<Pair>>();


        public void Add(K key, V value) {
            if (value==null){
                this.Remove(key);
                return;
            }//endif

            List<Pair> list = null;
            dic.TryGetValue(key.GetHashCode(), out list);
            if (list == null) {
                list = new List<Pair>();
                dic.Add(key.GetHashCode(), list);
            }//endif

            Boolean isDirty = false;            
            foreach(Pair p in list){
                if (p.Key.Target == null) {
                    isDirty = true;
                    continue;
                }//endif
                if (p.Key.Target == (Object)key) {
                    p.Value = (Object)value;
                    if (isDirty) cleanList(list);
                    return;
                }//endif
            }//for
            if (isDirty) cleanList(list);

            Pair newP=new Pair();
            newP.Key = new WeakReference(key);
            newP.Value = value;
            list.Add(newP);
        }//method


        public bool ContainsKey(K key) {
            List<Pair> list = null;
            dic.TryGetValue(key.GetHashCode(), out list);
            if (list == null) return false;

            Boolean isDirty = false;
            foreach (Pair p in list) {
                if (p.Key.Target == null) {
                    isDirty = true;
                    continue;
                }//endif
                if (p.Key.Target == (Object)key) {
                    if (isDirty) cleanList(list);
                    return true;
                }//endif
            }//for
            if (isDirty) cleanList(list);

            return false;
        }//method



        private void cleanList(List<Pair> list) {
            var temp = (from Pair p in list where p.Key.Target != null select p);
            list.Clear();
            list.AddRange(temp);
        }//method



        public bool Remove(K key) {
            List<Pair> list = null;
            dic.TryGetValue(key.GetHashCode(), out list);
            if (list == null) return true;

            foreach (Pair p in list) {
                if (p.Key.Target == (Object)key) {
                    p.Value = null;
                    break;
                }//endif
            }//for
            cleanList(list);

            return true;
        }//method





        public V this[K key] {
            get {
                List<Pair> list = null;
                dic.TryGetValue(key.GetHashCode(), out list);
                if (list == null) return default(V);

                Boolean isDirty = false;
                foreach (Pair p in list) {
                    if (p.Key.Target == null) {
                        isDirty = true;
                        continue;
                    }//endif

                    if (p.Key.Target == (Object)key) {
                        if (isDirty) cleanList(list);
                        return (V)p.Value;
                    }//endif
                }//for
                if (isDirty) cleanList(list);

                return default(V);
            }
            set {
                this.Add(key, value);
            }
        }


        public void Add(KeyValuePair<K, V> item) {
            throw new NotImplementedException();
        }

        public void Clear() {
            dic.Clear();
        }

        public bool Contains(KeyValuePair<K, V> item) {
            throw new NotImplementedException();
        }

        public void CopyTo(KeyValuePair<K, V>[] array, int arrayIndex) {
            throw new NotImplementedException();
        }

        public int Count {
            get {
                throw new NotImplementedException();            
                //return dic.Count();           
            }
        }

        public bool IsReadOnly {
            get { return false; }
        }

        public bool Remove(KeyValuePair<K, V> item) {
            throw new NotImplementedException();
        }



        public IEnumerator<KeyValuePair<K, V>> GetEnumerator() {
            throw new NotImplementedException();    
            //return dic.GetEnumerator();
        }


        //System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() {
        //    return ((System.Collections.IEnumerable)dic).GetEnumerator();
        //}





    }//class



    public class Pair{
        public WeakReference Key;
        public Object Value;
    }//method

}

【讨论】:

    【解决方案6】:

    这是我的并发弱(值)字典版本:

    public class WeakConcurrentDictionary<TKey, TValue> : IDictionary<TKey, TValue> where TValue : class
    {
        private readonly ConcurrentDictionary<TKey, WeakReference<TValue>> _internalDictionary =
            new ConcurrentDictionary<TKey, WeakReference<TValue>>();
    
        public TValue this[TKey key]
        {
            get
            {
                if (_internalDictionary.TryGetValue(key, out var weakReference) &&
                    weakReference.TryGetTarget(out var value))
                    return value;
    
                return null;
            }
            set
            {
                _internalDictionary.TryAdd(key, new WeakReference<TValue>(value));
            }
        }
    
        public ICollection<TKey> Keys => _internalDictionary.Keys;
    
        public ICollection<TValue> Values => _internalDictionary.Values
            .Select(_ => _.GetTarget())
            .Where(_ => _ != null)
            .ToList();
    
        public int Count => _internalDictionary.Count;
    
        public bool IsReadOnly => false;
    
        public void Add(TKey key, TValue value)
        {
            Purge();
            if (!_internalDictionary.TryAdd(key, new WeakReference<TValue>(value)))
            {
                throw new InvalidOperationException("Key already existing");
            }
        }
    
        public void Add(KeyValuePair<TKey, TValue> item)
        {
            throw new NotSupportedException();
        }
    
        public void Clear()
        {
            _internalDictionary.Clear();
        }
    
        public bool Contains(KeyValuePair<TKey, TValue> item) => _internalDictionary.TryGetValue(item.Key, out var weakReference) &&
                    weakReference.GetTarget() == item.Value;
    
        public bool ContainsKey(TKey key) => _internalDictionary.TryGetValue(key, out var weakReference) &&
                    weakReference.IsAlive();
    
        public void CopyTo(KeyValuePair<TKey, TValue>[] array, int arrayIndex)
        {
            Purge();
            _internalDictionary
                .Select(_ => new KeyValuePair<TKey, TValue>(_.Key, _.Value.GetTarget()))
                .Where(_ => _.Value != null)
                .ToList()
                .CopyTo(array, arrayIndex);
        }
    
        public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator()
        {
            Purge();
            return _internalDictionary
                .Select(_ => new KeyValuePair<TKey, TValue>(_.Key, _.Value.GetTarget()))
                .Where(_ => _.Value != null)
                .GetEnumerator();
        }
    
        public bool Remove(TKey key)
        {
            return _internalDictionary.TryRemove(key, out var weakReference);
        }
    
        public bool Remove(KeyValuePair<TKey, TValue> item)
        {
            throw new NotSupportedException();
        }
    
        public bool TryGetValue(TKey key, out TValue value)
        {
            value = null;
            if (_internalDictionary.TryGetValue(key, out var weakReference))
            {
                value = weakReference.GetTarget();
            }
    
            return value != null;
        }
    
        IEnumerator IEnumerable.GetEnumerator()
        {
            Purge();
            return GetEnumerator();
        }
    
        public void Purge()
        {
            foreach (var itemToRemove in _internalDictionary
                .Select(_ => new KeyValuePair<TKey, TValue>(_.Key, _.Value.GetTarget()))
                .Where(_ => _.Value == null))
            {
                _internalDictionary.TryRemove(itemToRemove.Key, out var weakReference);
            }
        }
    }
    
    public static class WeakReferenceExtensions
    {
        public static bool IsAlive<T>([NotNull] this WeakReference<T> weakReference) where T : class =>
            weakReference.TryGetTarget(out var target);
    
        public static T GetTarget<T>([NotNull] this WeakReference<T> weakReference, T defaultValue = default(T)) where T : class
        {
            if (!weakReference.TryGetTarget(out T target))
                return defaultValue;
    
            return target;
        }
    }
    

    还有一个测试证明对 value 的引用实际上被丢弃了:

        [TestMethod]
        public void TestWeakDictionary()
        {
            var weakDict = new WeakConcurrentDictionary<string, TestItem>();
    
            {
                var testItem = new TestItem();
                weakDict.Add("testitem", testItem);
    
                Assert.AreEqual(1, weakDict.Count);
                Assert.AreSame(testItem, weakDict["testitem"]);
            }
    
            GC.Collect();
            Assert.IsNull(weakDict["testitem"]);
            weakDict.Purge();
            Assert.AreEqual(0, weakDict.Count);
        }
    

    一些注意事项:

    1. Property Keys 会返回所有键,即使是那些其值已被收集的条目,但 Values 始终会返回实时的非空对象。
    2. this[key] 可以返回 null
    3. 您可以选择调用 Purge 来清除已收集值的条目
    4. 在发布模式下编译和执行时测试有效

    【讨论】:

      【解决方案7】:

      如果无法使用身份比较,则 ConditionalWeakTable 不是一个选项。

      在这种情况下,我敢于建议我们的实施 WeakTable.cs, 以及我们在博客中的描述 WeakTable.

      【讨论】:

        猜你喜欢
        • 2010-11-08
        • 2020-08-17
        • 1970-01-01
        • 1970-01-01
        • 2010-10-18
        • 2012-04-01
        • 2011-04-01
        • 1970-01-01
        • 2020-10-21
        相关资源
        最近更新 更多