Locking on Value Types

Locking is a core principle when it comes to multi-threaded applications, it allows a program make sure a block of code is executed by only one thread at at time. The lock statement in C#, or SyncLock in VB.net, is a really useful command to quickly create a lock and guarantee that the lock is released when the code finishes, even if it throws an exception.

private static object _lockObject = new object();
private void SomeFunction()
{ 
    lock(_lockObject)
    {
        // Do something
    }
}

There’s one thing that’ll trip up people new to threading, which is that the .net runtime disallows the locking on value types. This is because the underlying logic for the locking requires a pointer (how reference types are referenced) to be able to distinguish between different locks. If you pass a value type in the runtime will box the value type, meaning you’ll get a different pointer every time you try and get a lock, meaning you’ll effectively have no lock.

I came up with a work-around for this recently, that allows the locking of any value type. The core part to the solution is using a dictionary to map the value type to a reference type that can be locked. The solution is complicated somewhat by some extra logic that removes an object from the dictionary once no more locks are held against it.

The usage of the class is simple, and somewhat similar to the lock statement. The GetLock uses the first parameter as a “grouping” and the second parameter as the key to lock against. The grouping is necessary because it would be fairly easy to run into a scenario where the same value type is locked against in two unrelated blocks of code, blocking unnecessarily.

using (Locker<int>.GetLock("GroupName", 10))
{
    // Do something
}

And even though Locker only exposes one static method, GetLock, there’s quite a bit to it to make sure that any chase conditions are avoided, and there’s minimal wasted memory by removing unused locks from the dictionary (though this version doesn’t clean up an unused lock dictionary, meaning that using lots of groups would never entirely free the memory. This is solvable, but I’ll leave it for you to fix)


    /// <summary>
    /// Provides functionality to allow locking on value types
    /// </summary>
    public sealed class Locker<T> : IDisposable
    {
        #region Private Static Fields

        private static Dictionary<string, Dictionary<T, Locker<T>>> _lockDictionaries
            = new Dictionary<string, Dictionary<T, Locker<T>>>();

        #endregion

        #region Private Fields

        private int _lockCount;
        private bool _isLocked;
        private bool _isReleased;

        #endregion

        #region Private Constructor

        private Locker() { }

        #endregion

        #region Private Methods

        private bool Lock()
        {
            Interlocked.Increment(ref _lockCount);
            Monitor.Enter(this);
            // This lock has already been 'released', don't use it
            if (_isReleased)
            {
                Monitor.Exit(this);
                return false;
            }
            _isLocked = true;
            return true;
        }

        private void Unlock()
        {
            if (_isLocked)
            {
                var lockCount = Interlocked.Decrement(ref _lockCount);
                if (lockCount == 0)
                {
                    // The _lockCount is 0, we can now mark it as released
                    // and rase the AllLocksReleased event
                    _isReleased = true;
                    OnAllLocksReleased();
                }
                _isLocked = false;
                Monitor.Exit(this);
            }
        }

        #endregion

        #region AllLocksReleased Event

        private event EventHandler AllLocksReleased;

        private void OnAllLocksReleased()
        {
            if (AllLocksReleased != null)
                AllLocksReleased(this, EventArgs.Empty);
        }

        #endregion

        #region IDisposable Members

        public void Dispose()
        {
            Unlock();
        }

        #endregion

        #region Public Static Methods

        /// <summary>
        /// Aquires a lock for the given lockKey. IDisposable.Dispose() must be called to release the lock.
        /// </summary>
        /// <param name="groupName"></param>
        /// <param name="lockKey"></param>
        public static IDisposable GetLock(string groupName, T lockKey)
        {
            var lockDictionary = GetLockDictionary(groupName);

            Locker<T> lockObject;
            do
            {
                lockObject = null;
                if (!lockDictionary.TryGetValue(lockKey, out lockObject))
                {
                    lock (_lockDictionaries)
                    {
                        if (!lockDictionary.TryGetValue(lockKey, out lockObject))
                        {
                            // The lockDictionary dosn't have the key we're looking for, create a new instance
                            lockObject = new Locker<T>();
                            // Make sure the instance is released once all the locks are released
                            lockObject.AllLocksReleased += (o, e) => lockDictionary.Remove(lockKey);
                            lockDictionary.Add(lockKey, lockObject);
                        }
                    }
                }
            }
            while (!lockObject.Lock());
            // Continue looping until we acquire an unreleased lock

            return lockObject;
        }

        #endregion

        #region Private Static Methods

        /// <summary>
        /// Get's a lock dictionary for the given name
        /// </summary>
        /// <param name="name">The name of the dictionary to get.</param>
        private static Dictionary<T, Locker<T>> GetLockDictionary(string name)
        {
            Dictionary<T, Locker<T>> lockDictionary;
            if(!_lockDictionaries.TryGetValue(name, out lockDictionary))
            {
                lock(_lockDictionaries)
                {
                    if (!_lockDictionaries.TryGetValue(name, out lockDictionary))
                    {
                        lockDictionary = new Dictionary<T, Locker<T>>();
                        _lockDictionaries.Add(name, lockDictionary);
                    }
                }
            }
            return lockDictionary;
        }

        #endregion

    }

This code is provided As Is with no warranty expressed or implied.

Comments are closed.