I have read a lot of docs and articles and posts all over the internet. Almost everyone and everywhere commits that SpinLock is faster for a short running pieces of code, but I made a test, and it appears to me that simple Monitor.Enter works faster than SpinLock.Enter (Test is compiled against .NET 4.5)
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading.Tasks;
using System.Linq;
using System.Globalization;
using System.ComponentModel;
using System.Threading;
using System.Net.Sockets;
using System.Net;
class Program
{
static int _loopsCount = 1000000;
static int _threadsCount = -1;
static ProcessPriorityClass _processPriority = ProcessPriorityClass.RealTime;
static ThreadPriority _threadPriority = ThreadPriority.Highest;
static long _testingVar = 0;
static void Main(string[] args)
{
_threadsCount = Environment.ProcessorCount;
Console.WriteLine("Cores/processors count: {0}", Environment.ProcessorCount);
Process.GetCurrentProcess().PriorityClass = _processPriority;
TimeSpan tsInterlocked = ExecuteInterlocked();
TimeSpan tsSpinLock = ExecuteSpinLock();
TimeSpan tsMonitor = ExecuteMonitor();
Console.WriteLine("Test with interlocked: {0} ms\r\nTest with SpinLock: {1} ms\r\nTest with Monitor: {2} ms",
tsInterlocked.TotalMilliseconds,
tsSpinLock.TotalMilliseconds,
tsMonitor.TotalMilliseconds);
Console.ReadLine();
}
static TimeSpan ExecuteInterlocked()
{
_testingVar = 0;
ManualResetEvent _startEvent = new ManualResetEvent(false);
CountdownEvent _endCountdown = new CountdownEvent(_threadsCount);
Thread[] threads = new Thread[_threadsCount];
for (int i = 0; i < threads.Length; i++)
{
threads[i] = new Thread(() =>
{
_startEvent.WaitOne();
for (int j = 0; j < _loopsCount; j++)
{
Interlocked.Increment(ref _testingVar);
}
_endCountdown.Signal();
});
threads[i].Priority = _threadPriority;
threads[i].Start();
}
Stopwatch sw = Stopwatch.StartNew();
_startEvent.Set();
_endCountdown.Wait();
return sw.Elapsed;
}
static SpinLock _spinLock = new SpinLock();
static TimeSpan ExecuteSpinLock()
{
_testingVar = 0;
ManualResetEvent _startEvent = new ManualResetEvent(false);
CountdownEvent _endCountdown = new CountdownEvent(_threadsCount);
Thread[] threads = new Thread[_threadsCount];
for (int i = 0; i < threads.Length; i++)
{
threads[i] = new Thread(() =>
{
_startEvent.WaitOne();
bool lockTaken;
for (int j = 0; j < _loopsCount; j++)
{
lockTaken = false;
try
{
_spinLock.Enter(ref lockTaken);
_testingVar++;
}
finally
{
if (lockTaken)
{
_spinLock.Exit();
}
}
}
_endCountdown.Signal();
});
threads[i].Priority = _threadPriority;
threads[i].Start();
}
Stopwatch sw = Stopwatch.StartNew();
_startEvent.Set();
_endCountdown.Wait();
return sw.Elapsed;
}
static object _locker = new object();
static TimeSpan ExecuteMonitor()
{
_testingVar = 0;
ManualResetEvent _startEvent = new ManualResetEvent(false);
CountdownEvent _endCountdown = new CountdownEvent(_threadsCount);
Thread[] threads = new Thread[_threadsCount];
for (int i = 0; i < threads.Length; i++)
{
threads[i] = new Thread(() =>
{
_startEvent.WaitOne();
bool lockTaken;
for (int j = 0; j < _loopsCount; j++)
{
lockTaken = false;
try
{
Monitor.Enter(_locker, ref lockTaken);
_testingVar++;
}
finally
{
if (lockTaken)
{
Monitor.Exit(_locker);
}
}
}
_endCountdown.Signal();
});
threads[i].Priority = _threadPriority;
threads[i].Start();
}
Stopwatch sw = Stopwatch.StartNew();
_startEvent.Set();
_endCountdown.Wait();
return sw.Elapsed;
}
}
On a server with 24 cores of 2.5 GHz this application compiled with x64 produced the following results:
Cores/processors count: 24
Test with interlocked: 1373.0829 ms
Test with SpinLock: 10894.6283 ms
Test with Monitor: 1171.1591 ms
Mutexes Are Faster Than Spinlocks.
Typically, the performance of fair spinlocks drops extremely when the number of threads competing for the same lock is greater than the number of CPU cores in the system. The reason for this is that the thread which is next in the sequence to acquire the lock might be sleeping.
One advantage of a ticket lock over other spinlock algorithms is that it is fair. The waiting threads are processed in a first-in first-out basis as the dequeue ticket integer increases, thus preventing starvation.
Spinlock is a type of lock that causes a thread attempting to obtain it to check for its availability while waiting in a loop continuously. On the other hand, a mutex is a program object designed to allow different processes may take turns sharing the same resource.
You are just not testing a scenario where SpinLock can improve the threading. The core idea behind a spin-lock is that a thread-context switch is very expensive operation, costing between 2000 and 10,000 cpu cycles. And that if it is likely that a thread can acquire a lock by waiting for a bit (spinning) then the extra cycles burned waiting can pay off by avoiding the thread context switch.
So basic requirements is that the lock is held for a very short time, which is true in your case. And that there are reasonable odds that the lock can be acquired. Which is not true in your case, the lock is heavily contested by no less than 24 threads. All spinning and burning core without having a chance to acquire the lock.
In this test Monitor will work best since it queues threads waiting to acquire the lock. They are suspended until one of them has a chance to acquire the lock, released from the wait queue when the lock is released. Giving them all a fair chance to take a turn, thus maximizing the odds that they'll all finish at the same time. Interlocked.Increment is not bad either but can't provide a fairness guarantee.
It can be pretty hard to judge whether Spinlock is the right approach up front, you have to measure. A concurrency analyzer is the right kind of tool.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With