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

# re: Thread Safe Dictionary Update

The problem with what you say about acquiring the read lock, and then releasing it before acquiring the write lock is simply because it opens the door for someone to insert the same value you are trying to insert before you acquire the write lock.  This would cause a duplicate key conflict and subsequent error.  

This is exactly the scenario that upgradeable read locks were designed for.  That is because noone can acquire a write lock before i've released my read lock.  Also, only one upgradeable lock can be held at a time, which prevents two threads waiting for a write lock while already holding an upgradeable read.

I believe this is a fundamentally important feature in readerwriterlockslim.

Saturday, February 14, 2009 4:02 PM by Brian Rudolph

# re: Thread Safe Dictionary Update

Have you done any performance/scalability tests of this?

When I look at the code it seems that the only methods that gain anything compared to a solution that just use a Monitor are CopyTo, Values and Keys (this is the methods that actually may hold the read lock for long periods) - and only if the dictionary contains a significant number of elements.

The reason is that acquiring a ReaderWriteLockSlim is about 50% more expensive than acquiring a Monitor. In addition a simple dictionary operation (e.g. TryLookup, Add, and Remove) only cost about 25-50% of acquiring a Monitor.

Thursday, March 19, 2009 7:24 AM by Alpha

# re: Thread Safe Dictionary Update

I have done some scale testing in a few isolated situations.  

In general, this solution is at least as fast as one using monitor with a small number of threads.  

When I run it in some larger environments, like our inbound or outbound xml feed processors and caching models, where we have lots of threads, this model easily wins.  I have seen read\write performance increases of 80%+ when we have a dozen or more threads working concurrently.  These are situations where our servers would sit reasonably idle due to read\write contention.  Now we can easily spin them up to 95%+ active.

As always, individual testing is important.

Monday, March 23, 2009 9:45 AM by Brian Rudolph

# re: Thread Safe Dictionary Update

For my use needed to add a cast...

       public static implicit operator Dictionary<TKey, TValue>(ThreadSafeDictionary<TKey, TValue> tsd)

       {

           using (new ReadOnlyLock(tsd.dictionaryLock))

           {

               return (Dictionary<TKey, TValue>)tsd.dict;

           }

       }

Wednesday, October 21, 2009 3:21 PM by dj_axl

# re: Thread Safe Dictionary Update

And to add to PeterZacho's post...

       public ThreadSafeDictionary(IDictionary<TKey, TValue> dic)

       {

           using (new WriteLock(this.dictionaryLock))

           {

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

           }

       }

Wednesday, October 21, 2009 5:05 PM by dj_axl

# re: Thread Safe Dictionary Update

Could you explain why you use:

 while (!lockAcquired)

   lockAcquired = locks.TryEnterWriteLock(1);

instead of a simple:

 locks.EnterWriteLock();

?

Tuesday, June 15, 2010 3:28 AM by lau_

# re: Thread Safe Dictionary Update

Good Question... Either would work technically, i was attempting to provide a simple place for anyone to extend the class to allow for timeouts.  Never finished the implementation, but it would be simple to do so.

Thursday, June 17, 2010 12:13 PM by Brian Rudolph

# re: Thread Safe Dictionary Update

I have been using the Thread Safe dictionary successfully for more than a year but recently I have started seeing more and more of these errors:

System.Threading.ThreadAbortException: Thread was being aborted.

  at System.Threading.WaitHandle.WaitOneNative(SafeWaitHandle waitHandle, UInt32 millisecondsTimeout, Boolean hasThreadAffinity, Boolean exitContext)

  at System.Threading.WaitHandle.WaitOne(Int64 timeout, Boolean exitContext)

  at System.Threading.ReaderWriterLockSlim.WaitOnEvent(EventWaitHandle waitEvent, UInt32& numWaiters, Int32 millisecondsTimeout)

  at System.Threading.ReaderWriterLockSlim.TryEnterUpgradeableReadLock(Int32 millisecondsTimeout)

I'm wondering what might be causing this problem???

Monday, July 26, 2010 2:40 PM by frJericho

# re: Thread Safe Dictionary Update

I should have added that the one thing that changed is that my application was previously running in a VM with only 1 CPU core and has recently been upgraded to a VM with 2 CPU cores.

Is it possible that this problem is due to two threads running are attempting to get a lock on the same resource?

Monday, July 26, 2010 3:33 PM by frJericho

# re: Thread Safe Dictionary Update

this error is technically being spawned from outside of the dictionary.  It says that the thread was being aborted, so I'd imagine that somewhere in your code an ThreadAbort is being done, which put the whole thing in a bit of an iffy state.  My guess is that you are perhaps doing some work in a Dispose method that touches the dictionary.  I have been using this dictionary for years and have never seen one of these.  Do you have a code snippet of how it's being used?

Thursday, July 29, 2010 4:32 PM by Brian Rudolph