Spinlock Class in Threading C#


Introduction

The .NET framework's ReaderWriterLock is slow. So slow that the overhead removes almost all benefits derived from it being a reader-writer lock (well, that is somewhat of an exaggeration). With the release of .NET framework 3.5, Microsoft introduced ReaderWriterLockSlim in an attempt to solve this problem. It is around 3 times faster than ReaderWriterLock (with no contention). Yet Microsoft did not design ReaderWriterLockSlim properly – it relies on an internal SpinLock. We can assume that the main reason you will be using a reader-writer lock is because there will be many concurrent readers and few writers, so a design which relies on a SpinLock even for concurrent shared (reader) acquisition will result in poor performance.

Spinlock

System.Threading.SpinLock provides a mutual exclusion lock primitive where a thread trying to acquire the lock waits in a loop repeatedly checking until the lock becomes available.

When there is contention, SpinLock does not block, but spins in user mode. This avoids blocking, which requires a kernel-mode lock. Kernel Locks always cause an expensive context switch. After the spin, ideally the contention has been removed and execution can continue without the expensive context switch. SpinLock is ideal in scenarios involving frequent contention with short waits.

System.Threading.SpinLock is a low-level mutual exclusion lock that you can use for scenarios that have very short wait times. SpinLock is not re-entrant. After a thread enters the lock, it must exit the lock correctly before it can enter again. Typically, any attempt to re-enter the lock would cause deadlock, and deadlocks can be very difficult to debug. As an aid to development, System.Threading.SpinLock supports a thread-tracking mode that causes an exception to be thrown when a thread attempts to re-enter a lock that it already holds. This lets you more easily locate the point at which the lock was not exited correctly. You can turn on thread-tracking mode by using the SpinLock constructor that takes a Boolean input parameter, and ing in an argument of true. After you complete the development and testing phases, turn off thread-tracking mode for better performance.

The SpinLock type exposes the following members.

Constructors

SpinLock()- The constructor used to Initializes a new instance of the SpinLock structure.

Properties

  • Isheld-  The property gets whether the lock is currently held by any thread.
  • IsheldByCurrentThread- The property gets whether the lock is held by the current thread.
  • IsThreadOwnerTrackingEnabled- The property gets whether thread ownership tracking is enabled for this instance.

Method

  • Enter(ref bool lockTaken)-  To acquire the lock.
  • Exit()- Release the lock.
  • Finalize()- This method allows an object to try to release resources and perform other cleanup operations before it is reclaimed by garbage collection. (Inherited from Object.)

The following code shows how to use the  Enter() and Exit() methods to acquire and release locks:

using System;
using System.Threading;
namespace SpinlockInthreading
{
    public class akshay
    {
        SpinLock spLock = new SpinLock();
        public void process()
        {
            bool lockTaken = false;
            try
            {
                spLock.Enter(ref lockTaken);
                Thread.Sleep(1000);
                Console.WriteLine("process method acquire the lock::");
            }
            finally
            {
                if (lockTaken) spLock.Exit();
              
Console.WriteLine("process method Release the lock::");
            }
        }
        public static void Main()
        {
            akshay ak = new akshay();
            Thread th = new Thread(new ThreadStart(ak.process));
            th.Start();
            Console.Read(); 
        }
    }
}

output

spinlock1.gif

However one difference with Monitor is, this lock is non reentrant in nature. In the following code the thread is trying to acquire the same SpinLock twice.

using System;
using System.Threading;
namespace SpinlockInthreading
{
    public class akshay
    {
        SpinLock spLock = new SpinLock();
        public void process()
        {
            bool lockTaken = false;
            try
            {
                spLock.Enter(ref lockTaken);
                Thread.Sleep(1000);
                process1();
                Console.WriteLine("process method acquire the lock::");
            }
            finally
            {
                if (lockTaken) spLock.Exit();
                
Console.WriteLine("process method Release the lock::");
            }
        }
        public void process1()
        {
            bool lockTaken = false;
            try
            {
                spLock.Enter(ref lockTaken);
                Console.WriteLine("process1 method acquire the lock::");
            }
            finally
            {
                if (lockTaken) spLock.Exit();
                
Console.WriteLine("process1 method Release the lock::");
            }
        }
        public static void Main()
        {
            akshay ak = new akshay();
            Thread th = new Thread(new ThreadStart(ak.process));
            //Thread th1 = new Thread(new ThreadStart(ak.process1));
            th.Start();
            //th1.Start();
            Console.Read(); 
        }
    }
}

Output

The execution of the method M1 will throw a LockRecursionException exception as:

synlock2.gif

However we can avoid this by checking whether a lock is held by the same thread or not using the SpinLock.IsHeldByCurrentThread property.

using System;
using System.Threading;
namespace SpinlockInthreading
{
    public class akshay
    {
        SpinLock spLock = new SpinLock();
        public void process()
        {
            bool lockTaken = false;
            try
            {
                spLock.Enter(ref lockTaken);
                Thread.Sleep(1000);
                process1();
                Console.WriteLine("process method acquire the lock::");
            }
            finally
            {
                if (lockTaken) spLock.Exit();
                
Console.WriteLine("process method Release the lock::");
            }
        }
        public void process1()
        {
            bool lockTaken = false;
            try
            {
               
if (!spLock.IsHeldByCurrentThread) spLock.Enter(ref lockTaken);
                Console.WriteLine("process1 method acquire the lock::");
            }
            finally
            {
                if (lockTaken) spLock.Exit();
                
Console.WriteLine("process1 method Release the lock::");
            }
        }
        public static void Main()
        {
            akshay ak = new akshay();
            Thread th = new Thread(new ThreadStart(ak.process));
            //Thread th1 = new Thread(new ThreadStart(ak.process1));
            th.Start();
            //th1.Start();
            Console.Read(); 
        }
    }
}

Output

spinlock3.gif

Usage

Spin locks can be used for leaf-level locks where the object allocation implied by using a Monitor, in size or due to garbage collection pressure, is overly expensive. A spin lock can be useful to avoid blocking; however, if you expect a significant amount of blocking, you should probably not use spin locks due to excessive spinning. Spinning can be beneficial when locks are fine-grained and large in number (for example, a lock per node in a linked list) and also when lock hold-times are always extremely short.

SpinLock should only be used after you have been determined that doing so will improve an application's performance. It is also important to note that SpinLock is a value type, for performance reasons. For this reason, you must be very careful not to accidentally copy a SpinLock instance, as the two instances (the original and the copy) would then be completely independent of one another, which would likely lead to erroneous behavior of the application. If a SpinLock instance must be ed around, it should be ed by reference rather than by value.

Limitation

In general, while holding a spin lock, one should avoid any of the following actions.

  •  Blocking.
  •  Calling anything that itself may block.
  •  Holding more than one spin lock at once.
  •  Making dynamically dispatched calls (interface and virtuals).
  •  Making statically dispatched calls into any code one doesn't own.
  •  Allocating memory.
  •  Do not store SpinLock instances in readonly fields.
Resources

Here some useful related resources.