Thread Safe Dictionary Update

 

For those of you that have read my post about the Thread Safe Dictionary, I have a few updates. 

In recent months, I have done a ridiculous amount of multi-threaded work.  This has forced me to expand my threading libraries.  One of the primary objects that required changes\fixes was my dictionary. 

 

The dictionary uses ReaderWriterLockSlim locks.  These are clean, lightweight locks that MS has provided.  Most importantly, they allow upgradeable read locks.  I now use simple IDisposable locking wrappers.  This allows us to use the "using" syntax to ensure lock releases.

It is important to note that no safe dictionary can provide safe inserts without the caller forcing a lock.  This is because by the time you check for the existence of an object and then insert it, it may have been inserted by another thread.  The only way to prevent this is by doing a "blind" merge.  The reason I call it blind, is that when you do a merge, we check for existence, and do a replace\insert within the scope of a single write lock.  This means you cannot control which objects get replaced in the dictionary.  This is rarely important in caching situations.

using System;
using System.Collections;
using System.Collections.Generic;
using System.Threading;


public interface IThreadSafeDictionary<TKey, TValue> : IDictionary<TKey, TValue>
{
    /// <summary>
    /// Merge is similar to the SQL merge or upsert statement.  
    /// </summary>
    /// <param name="key">Key to lookup</param>
    /// <param name="newValue">New Value</param>
    void MergeSafe(TKey key, TValue newValue);


    /// <summary>
    /// This is a blind remove. Prevents the need to check for existence first.
    /// </summary>
    /// <param name="key">Key to Remove</param>
    void RemoveSafe(TKey key);
}


[Serializable]
public class ThreadSafeDictionary<TKey, TValue> : IThreadSafeDictionary<TKey, TValue>
{
    //This is the internal dictionary that we are wrapping
    IDictionary<TKey, TValue> dict = new Dictionary<TKey, TValue>();


    [NonSerialized]
    ReaderWriterLockSlim dictionaryLock = Locks.GetLockInstance(LockRecursionPolicy.NoRecursion); //setup the lock;


    /// <summary>
    /// This is a blind remove. Prevents the need to check for existence first.
    /// </summary>
    /// <param name="key">Key to remove</param>
    public void RemoveSafe(TKey key)
    {
        using (new ReadLock(this.dictionaryLock))
        {
            if (this.dict.ContainsKey(key))
            {
                using (new WriteLock(this.dictionaryLock))
                {
                    this.dict.Remove(key);
                }
            }
        }
    }


    /// <summary>
    /// Merge does a blind remove, and then add.  Basically a blind Upsert.  
    /// </summary>
    /// <param name="key">Key to lookup</param>
    /// <param name="newValue">New Value</param>
    public void MergeSafe(TKey key, TValue newValue)
    {
        using (new WriteLock(this.dictionaryLock)) // take a writelock immediately since we will always be writing
        {
            if (this.dict.ContainsKey(key))
            {
                this.dict.Remove(key);
            }


            this.dict.Add(key, newValue);
        }
    }


    public virtual bool Remove(TKey key)
    {
        using (new WriteLock(this.dictionaryLock))
        {
            return this.dict.Remove(key);
        }
    }


    public virtual bool ContainsKey(TKey key)
    {
        using (new ReadOnlyLock(this.dictionaryLock))
        {
            return this.dict.ContainsKey(key);
        }
    }


    public virtual bool TryGetValue(TKey key, out TValue value)
    {
        using (new ReadOnlyLock(this.dictionaryLock))
        {
            return this.dict.TryGetValue(key, out value);
        }
    }


    public virtual TValue this[TKey key]
    {
        get
        {
            using (new ReadOnlyLock(this.dictionaryLock))
            {
                return this.dict[key];
            }
        }
        set
        {
            using (new WriteLock(this.dictionaryLock))
            {
                this.dict[key] = value;
            }
        }
    }


    public virtual ICollection<TKey> Keys
    {
        get
        {
            using (new ReadOnlyLock(this.dictionaryLock))
            {
                return new List<TKey>(this.dict.Keys);
            }
        }
    }


    public virtual ICollection<TValue> Values
    {
        get
        {
            using (new ReadOnlyLock(this.dictionaryLock))
            {
                return new List<TValue>(this.dict.Values);
            }
        }
    }


    public virtual void Clear()
    {
        using (new WriteLock(this.dictionaryLock))
        {
            this.dict.Clear();
        }
    }


    public virtual int Count
    {
        get
        {
            using (new ReadOnlyLock(this.dictionaryLock))
            {
                return this.dict.Count;
            }
        }
    }


    public virtual bool Contains(KeyValuePair<TKey, TValue> item)
    {
        using (new ReadOnlyLock(this.dictionaryLock))
        {
            return this.dict.Contains(item);
        }
    }


    public virtual void Add(KeyValuePair<TKey, TValue> item)
    {
        using (new WriteLock(this.dictionaryLock))
        {
            this.dict.Add(item);
        }
    }


    public virtual void Add(TKey key, TValue value)
    {
        using (new WriteLock(this.dictionaryLock))
        {
            this.dict.Add(key, value);
        }
    }


    public virtual bool Remove(KeyValuePair<TKey, TValue> item)
    {
        using (new WriteLock(this.dictionaryLock))
        {
            return this.dict.Remove(item);
        }
    }


    public virtual void CopyTo(KeyValuePair<TKey, TValue>[] array, int arrayIndex)
    {
        using (new ReadOnlyLock(this.dictionaryLock))
        {
            this.dict.CopyTo(array, arrayIndex);
        }
    }


    public virtual bool IsReadOnly
    {
        get
        {
            using (new ReadOnlyLock(this.dictionaryLock))
            {
                return this.dict.IsReadOnly;
            }
        }
    }


    public virtual IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator()
    {
        throw new NotSupportedException("Cannot enumerate a threadsafe dictionary.  Instead, enumerate the keys or values collection");
    }


    IEnumerator IEnumerable.GetEnumerator()
    {
        throw new NotSupportedException("Cannot enumerate a threadsafe dictionary.  Instead, enumerate the keys or values collection");
    }
}


public static class Locks
{
    public static void GetReadLock(ReaderWriterLockSlim locks)
    {
        bool lockAcquired = false;
        while (!lockAcquired)
            lockAcquired = locks.TryEnterUpgradeableReadLock(1);
    }


    public static void GetReadOnlyLock(ReaderWriterLockSlim locks)
    {
        bool lockAcquired = false;
        while (!lockAcquired)
            lockAcquired = locks.TryEnterReadLock(1);
    }


    public static void GetWriteLock(ReaderWriterLockSlim locks)
    {
        bool lockAcquired = false;
        while (!lockAcquired)
            lockAcquired = locks.TryEnterWriteLock(1);
    }


    public static void ReleaseReadOnlyLock(ReaderWriterLockSlim locks)
    {
        if (locks.IsReadLockHeld)
            locks.ExitReadLock();
    }


    public static void ReleaseReadLock(ReaderWriterLockSlim locks)
    {
        if (locks.IsUpgradeableReadLockHeld)
            locks.ExitUpgradeableReadLock();
    }


    public static void ReleaseWriteLock(ReaderWriterLockSlim locks)
    {
        if (locks.IsWriteLockHeld)
            locks.ExitWriteLock();
    }


    public static void ReleaseLock(ReaderWriterLockSlim locks)
    {
        ReleaseWriteLock(locks);
        ReleaseReadLock(locks);
        ReleaseReadOnlyLock(locks);
    }


    public static ReaderWriterLockSlim GetLockInstance()
    {
        return GetLockInstance(LockRecursionPolicy.SupportsRecursion);
    }


    public static ReaderWriterLockSlim GetLockInstance(LockRecursionPolicy recursionPolicy)
    {
        return new ReaderWriterLockSlim(recursionPolicy);
    }
}


public abstract class BaseLock : IDisposable
{
    protected ReaderWriterLockSlim _Locks;


    public BaseLock(ReaderWriterLockSlim locks)
    {
        _Locks = locks;
    }


    public abstract void Dispose();
}


public class ReadLock : BaseLock
{
    public ReadLock(ReaderWriterLockSlim locks)
        : base(locks)
    {
        Locks.GetReadLock(this._Locks);
    }


    public override void Dispose()
    {
        Locks.ReleaseReadLock(this._Locks);
    }
}


public class ReadOnlyLock : BaseLock
{
    public ReadOnlyLock(ReaderWriterLockSlim locks)
        : base(locks)
    {
        Locks.GetReadOnlyLock(this._Locks);
    }


    public override void Dispose()
    {
        Locks.ReleaseReadOnlyLock(this._Locks);
    }
}


public class WriteLock : BaseLock
{
    public WriteLock(ReaderWriterLockSlim locks)
        : base(locks)
    {
        Locks.GetWriteLock(this._Locks);
    }


    public override void Dispose()
    {
        Locks.ReleaseWriteLock(this._Locks);
    }
}

 

Published Monday, September 29, 2008 11:14 AM by Brian Rudolph
Filed under: ,

Comments

# Thread Safe Dictionary Using ReaderWriterLockSlim

You've been kicked (a good thing) - Trackback from DotNetKicks.com

Monday, November 03, 2008 1:17 PM by DotNetKicks.com

# re: Thread Safe Dictionary Update

<a href="www.dotnetkicks.com/kick src="www.dotnetkicks.com/.../KickItImageGenerator.ashx border="0" alt="kick it on DotNetKicks.com" /></a>

Monday, November 03, 2008 1:17 PM by David Justice

# re: Thread Safe Dictionary Update

There's a problem with this implementation in that it claims that it is serializable.  If the object is serialized, it looses its ReaderWriterLockSlim instance once it is deserialized.  Upon deserialization, you'll start to encounter NullReferenceExcpetions when you call methods.

Also, do you have a test instance that requires the ReaderWriterLockSlim instance to support lock recursion?  I couldn't find any reason for this in your implementation of ThreadSafeDictionary.

Thursday, November 20, 2008 1:45 PM by mnero0429

# re: Thread Safe Dictionary Update

Interesting observations. The recursion issue i had already dealt with but had not posted.  The Serialization issue I had not run across.  I failed to recognize that the default constructor would not be called on binary deserialization.

I have posted the updates.  Thanks for the feedback.

Wednesday, November 26, 2008 1:47 PM by Brian Rudolph

# re: Thread Safe Dictionary Update

Thanks for the code, Brian. While using it I realized that a couple of constructors are missing - the following changes helped me:

   //This is the internal dictionary that we are wrapping

   IDictionary<TKey, TValue> dict;

   [NonSerialized]

   ReaderWriterLockSlim dictionaryLock = Locks.GetLockInstance(LockRecursionPolicy.NoRecursion); //setup the lock;

   public ThreadSafeDictionary()

   {

       dict = new Dictionary<TKey, TValue>();

   }

   public ThreadSafeDictionary(int capacity)

   {

       dict = new Dictionary<TKey, TValue>(capacity);

   }

   public ThreadSafeDictionary(ThreadSafeDictionary<TKey, TValue> original)

   {

       dict = new Dictionary<TKey, TValue>(original.dict);

   }

Sunday, December 28, 2008 1:21 AM by PeterZacho

# re: Thread Safe Dictionary Update

Hi Brain, can you explain why you are dooing a Busy-wait when acquiring a lock (the while loops)? This seems to be quite expensive.

I also noticed that you used the upgradable read lock when dooing double check locking. While this is better than directly getting a Write lock, I would do the first check with just a read lock, release the read lock, and then acquire the write lock to do the update. This allows for better concurrency without casing deadlocks.

You might also want to check out what microsoft is doing in PFX, I wrote a blog about their ConcurrentDictionay last week blogs.infosupport.com/.../Implementing-a-Thread-Safe-cache-using-the-Parallel-Extensions.aspx

Friday, January 02, 2009 5:15 AM by FrankB