Thread Safe Dictionary in .NET with ReaderWriterLockSlim

Since MS has decided to not include a thread-safe dictionary in the .NET framework, many of us have been forced to implement our own.  What MS has done now with .NET 3.5 is given us a newer locking mechanism which makes this task easier. 

Previously, we had 2 lock mechanisms, neither of which worked very well for my uses.  The first is the Lock statement.  While this method is effective, it causes serious contention issues with simultaneous reads\writes.  Since it is generally safe to execute multiple concurrent reads from a dictionary, this was a huge performance flaw as the Lock statement was forcing us to perform all actions against the object in a serial fashion.

So that left ReaderWriterLock, which would allow us to take out Reader & Writer locks independently.  This seemed to be the answer.  Unfortunately, after further testing and review, MS and the dev communities warned against this lock type as it could have unintended consequences in certain situations.  The explanations for these I will not go in to, a simple Google search will answer those questions.  For the most part, I don't have the mental capacity to understand, much less explain the failures.  I will tell you that I have tested this method, and ran in to some issues, and subsequently abandoned the approach.

With .NET 3.5 comes the brand-spankin-new ReaderWriterLockSlim.  ReaderWriterLockSlim is similar to the ReaderWriterLock in functionality, minus some of the drawbacks.  Of course any new approach will bring on its own consequences, and if you do a little research, you will find that Slim isn't infallible either. 

Many of our applications implement the Factory model.  Factory models are great, they are extensible, and they can be extremely performant.  With that said, any data driven application that uses the Factory model without a caching model, will most likely perform poorly.  So, in our factories, we build in a caching mechanism.  A very simple type and ID based cache that allows us to persist commonly used objects to in-memory cache for a specific amount of time.  I won't go in to the implementation of this model here as every caching implementation has its own needs and wants.

At the heart of this and likely any caching model is inevitably a Dictionary or Dictionary like structure.  MS programmers have spent countless hours fine-tuning the Dictionary class to make it as performant as possible.  I challenge anyone to write a managed Dictionary that performs as well in such a wide variety of scenarios.  I've tried and failed.  Unfortunately, MS has not provided a thread-safe implementation.  Generally when you run into the question of thread-safety, most people have no idea what you are talking about.  The problem with writing truly thread-safe code that can work in any scenario is that you will sacrifice performance.  If you tailor your thread-safe mechanisms to your particular problem, you can generally maintain a higher level of performance.

The reason we need thread-safety is not that we are specifically spawning threads.  In IIS, worker processes are inherently multi-threaded.  Therefore, using static objects in a web-farm generally requires some level of thread safety, otherwise you will get dreaded "An item with the same key has already been added" errors.  This is because between the time you check to see if a key already exists in your dictionary and the time you insert it, some other thread may have inserted this key.  This is especially the case in object factories, because you have multiple threads attempting to access/create objects and subsequently cache them.

This is the code I've written.  Update: This code has been tested, and is working well.  I will keep this post updated with any changes and test results.  I also have not made this class utilize lock escalation, which ReaderWriterLockSlim supports.  As always, please perform your own testing.

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


 

[Update]
Through some testing, I found the need to update the ThreadSafe Dictionary.  I changed all of the locking to use IDisposables to ensure lock creation and destruction.

 

Published Friday, September 26, 2008 10:14 AM by Brian Rudolph

Comments

# 蛙蛙推荐:设计一个高效的网络服务器用户管理类

摘要:做一个有状态的网络服务端应用,一般需要维护一个在线用户列表,每次用户登录、注销都要修改这个列表,还得考虑超时清理的逻辑,对这个列表的操作大多时候需要用锁来进行线程同步,我们试图来用一种不需要线程同步的方法来做到这些事情。

Friday, April 10, 2009 11:39 AM by 蛙蛙池塘

# Twitter Trackbacks for Thread Safe Dictionary in .NET with ReaderWriterLockSlim - What are we going to do tomorrow night, Brain? [dotnetcommandos.com] on Topsy.com

Pingback from  Twitter Trackbacks for                 Thread Safe Dictionary in .NET with ReaderWriterLockSlim - What are we going to do tomorrow night, Brain?         [dotnetcommandos.com]        on Topsy.com

# WPF for the Web guy

WPF for the Web guy

Wednesday, January 20, 2010 3:01 PM by mostlylucid

# I don&#8217;t have the mental capacity to understand &laquo; Malaysian&#039;s Chromosome

Pingback from  I don’t have the mental capacity to understand «  Malaysian's Chromosome

# Thread Safe Dictionary in .NET with ReaderWriterLockSlim

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

Thursday, May 06, 2010 4:06 AM by DotNetKicks.com

# Canon printer driver - Canon s520 - C3380i canon driver ir printer

Pingback from  Canon printer driver - Canon s520 - C3380i canon driver ir printer