A couple of times recently we have had a requirement to lock on a named item. We have not required a distributed lock (either between app domains or between servers), simply an in memory lock on a specific item.

There is no standard way to achieve this in .Net and getting the details correct is surprisingly difficult (as with all multi-threaded code) so I will share the two versions we came up with.

Version 1 uses a standard Dictionary of lock objects. This dictionary stores the lock object being used for the key in question, as well as a reference count, i.e the number of threads attempting to take the lock. This is important, without storing a count of references you cannot ensure the lock object is removed cleanly when the last reference is released. Access to the dictionary is controlled by a standard global lock object, but this should be very low contention as it is only taken whilst the dictionary is updated. If the body of work done in the lock is very small, then this is probably not an appropriate solution and techniques such as lock striping should be considered.

Firstly some shared code between the two versions:

    public interface ILock<T>
    {
        IDisposable Enter(T id);
    }

    public class ActionDisposable : IDisposable
    {
        private readonly Action action;

        public ActionDisposable(Action action)
        {
            if (action == null)
            {
                throw new ArgumentNullException("action");
            }

            this.action = action;
        }

        public void Dispose()
        {
            action();
        }
    }

Version 1:


    public class NamedItemLock<T> : ILock<T>
    {

        private readonly Dictionary<T, Tuple<object, int>> locks = new Dictionary<T, Tuple<object, int>>();
        private readonly object globalLock = new object();

        public IDisposable Enter(T id)
        {
            object lockHandle;

            lock (globalLock)
            {
                if (!locks.ContainsKey(id))
                {
                    locks[id] = Tuple.Create(new object(),1);
                }
                else
                {
                    var lockCount = locks[id];
                    locks[id] = Tuple.Create(lockCount.Item1, lockCount.Item2 + 1);
                }
                lockHandle = locks[id].Item1;
            }

            //Any exceptions dealing with the dictionary up to this point are fine as the lock has not been taken
            //we need to be more careful after this point

            bool lockTaken = false;
            try
            {
                //Monitor.Enter can throw an exception AND take the lock
                //we need to catch this and exit the lock, making sure we rethrow the exception as well
                Monitor.Enter(lockHandle, ref lockTaken);
            }
            catch
            {
                if (lockTaken)
                {
                    Monitor.Exit(lockHandle);
                    throw;
                }
            }

            try
            {
                //don't think this call should ever fail but lets make sure we release the lock if it has
                return new ActionDisposable(() => exit(id, lockHandle));
            }
            catch
            {
                Monitor.Exit(lockHandle);
                throw;
            }
        }

        private void exit(T id, object lockHandle)
        {
            //We need to be careful here that we actually release the lock
            //since the dictionary might blow up, we actually pass the lockHandle to this method as well
            //any exception occuring before we have managed to update the dictionary
            //is caught and the lock released anyway. 
            //The worst case scenario is we will end up with the dictionary saying there are outstanding locks for a certain key
            //Whereas there are actually none
            //This won't cause a deadlock, but may cause a minor memory leak

            lock (globalLock)
            {
                Tuple<object,int> lockCount = default(Tuple<object,int>);
                try
                {
                    lockCount = locks[id];
                    if (lockCount.Item2 == 1)
                    {
                        locks.Remove(id);
                    }
                    else
                    {
                        locks[id] = Tuple.Create(lockCount.Item1, lockCount.Item2 - 1);
                    }
                }
                catch
                {
                    Monitor.Exit(lockHandle);
                    throw;
                }
                Monitor.Exit(lockCount.Item1);
            }
        }
    }

The second version (courtesy of Steve) uses the ConcurrentDictionary class and simply spins until we successfully add the new item, anything else trying to take the lock will spin until this thread releases it.

    public class NamedItemLockSpin<T> : ILock<T>
    {

        private readonly ConcurrentDictionary<T, object> locks = new ConcurrentDictionary<T, object>();

        private readonly int spinWait;

        public NamedItemLockSpin(int spinWait)
        {
            this.spinWait = spinWait;
        }

        public IDisposable Enter(T id)
        {
            while(!locks.TryAdd(id, new object()))
            {
                Thread.SpinWait(spinWait);
            }

            return new ActionDisposable(() => exit(id));
        }

        private void exit(T id)
        {
            object obj;
            locks.TryRemove(id, out obj);
        }
    }

I expected the spin lock version to be slightly slower when the lock needs to be held for a long time (i.e high contention and long waits), however in testing there is not much between the two.

Usage is fairly straightforward:

            var namedLocks = new NamedItemLock<int>();

            using (var sync = namedLocks.Enter(10))
            {
                //do something for item 10
            }

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

w

Connecting to %s