Waitable Lock (C#)

A nice touch to the Python Lock class interface is the ability to specify whether or not an acquisition attempt should block, waiting until the lock is acquired, or just fall through if it is not available. The latter functionality is also known as “test and set”.

In C# the blocking part can be done with a AutoResetEvent, while the testandset can be achieved with an actual variable test and set (rather, a CAS: Interlocked.CompareExchange). However, there is no readily available mutex object that provides both functionalities at the same time, as Python’s Lock does.

So, I rolled my own. A custom (thread-safe) C# lock that supports test and set as well as blocking acquisition. Bonus feature: non-modifying blocking and events for locking and unlocking (useful for waiting until a certain event took place).

As always, be careful when using this. Note that the WaitWhile(Un)Locked functions, specifically, can get you in trouble. There is no way of knowing when a call to that function has actually started registering the corresponding event. Thread scheduling could cause it to still be hanging at the function entry without actually having done anything. Meanwhile, another thread that you wanted to wait for may be firing away and doing its job, unbeknownst to your monitoring thread. Avoid this, for example by ensuring that a thread would not flip the state twice (e.g. one locker thread, and one unlocker thread, waiting for eachother).

Better yet, use queues and similar thread safe data channels to pass around data. This lock is intended for simple, boolean synchronization.

using System;
using System.Threading;

namespace Locking
{
    internal interface IWaitableLock
    {
        /// <summary>
        /// Acquire the lock.
        /// </summary>
        /// <param name="blocking">Whether to wait until acquired or return immediately</param>
        /// <returns>true when acquired, false otherwise</returns>
        bool Acquire(bool blocking);
        /// <summary>
        /// Release the lock.
        /// </summary>
        /// <param name="strict">Whether to assert that the lock is acquired.</param>
        /// <exception cref="SynchronizationLockException">
        /// Strictly unlocking while already unlocked.
        /// </exception>
        /// <returns>The state of the lock before this action took place (atomically).</returns>
        bool Release(bool strict);
        bool IsLocked();
        // TODO: These two do not belong here, better use thread-safe queues.
        /// <summary>
        /// Return as soon as it detects the lock is acquired. Does not affect the lock.
        /// </summary>
        void WaitWhileUnlocked();
        /// <summary>
        /// Return as soon as it detects the lock is released. Does not affect the lock.
        /// </summary>
        void WaitWhileLocked();
        /// <summary>
        /// Event called after the lock has been acquired
        /// </summary>
        event Action Acquired;
        /// <summary>
        /// Event called after the lock has been released
        /// </summary>
        event Action Released;
    }

    /// <summary>
    /// Simple boolean mutex, like Python's threading.Lock.
    /// </summary>
    internal class MyLock : IWaitableLock
    {
        private int locked;
        public event Action Acquired = delegate { };
        public event Action Released = delegate { };

        public MyLock()
        {
            locked = 0;
        }

        public bool Acquire(bool wait)
        {
            if (Interlocked.CompareExchange(ref locked, 1, 0) == 0)
            {
                Acquired();
                return true;
            }
            else if (wait)
            {
                WaitWhileLocked();
                // The lock was released, try (!) to acquire again.
                return Acquire(wait);
            }
            else
            {
                return false;
            }
        }

        public bool Release(bool strict)
        {
            bool waslocked = (Interlocked.CompareExchange(ref locked, 0, 1) == 1);
            if (waslocked)
            {
                Released();
            }
            else if (strict)
            {
                throw new SynchronizationLockException("Already unlocked");
            }
            return waslocked;
        }

        public bool IsLocked()
        {
            return locked == 1;
        }

        public void WaitWhileUnlocked()
        {
            var wait = new AutoResetEvent(false);
            Action set = (() => wait.Set()); // ignore return value
            Acquired += set;
            try
            {
                if (locked == 1)
                {
                    return;
                }
                wait.WaitOne();
            }
            finally
            {
                Acquired -= set;
            }
        }

        public void WaitWhileLocked()
        {

            var wait = new AutoResetEvent(false);
            Action set = (() => wait.Set()); // ignore return value
            Released += set;
            try
            {
                if (locked == 0)
                {
                    return;
                }
                wait.WaitOne();
            }
            finally
            {
                Released -= set;
            }
        }
    }
}


Or leave a comment below: