¿Por qué todos afirman que SpinLock es más rápido?

He leído un montón de documentos y artículos y publicaciones en todo el Internet. Casi todos y en todas partes se comprometen a que SpinLock es más rápido para un trozo de código de ejecución corta, pero hice una prueba y me parece que Monitor.Enter simple funciona más rápido que SpinLock.Enter (La prueba está comstackda contra .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  { _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  { _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  { _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; } } 

En un servidor con 24 núcleos de 2,5 GHz, esta aplicación comstackda con x64 produjo los siguientes resultados:

 Cores/processors count: 24 Test with interlocked: 1373.0829 ms Test with SpinLock: 10894.6283 ms Test with Monitor: 1171.1591 ms 

Simplemente no está probando un escenario donde SpinLock puede mejorar el subproceso. La idea central detrás de un locking de giro es que un cambio de contexto de subproceso es una operación muy costosa, que cuesta entre 2000 y 10.000 ciclos de CPU. Y si es probable que un subproceso pueda adquirir un locking al esperar un poco (giro), los ciclos adicionales que se están quemando en espera pueden dar sus frutos al evitar el cambio de contexto del subproceso.

Por lo tanto, los requisitos básicos es que el locking se mantiene por un tiempo muy corto, lo cual es cierto en su caso. Y que hay probabilidades razonables de que la cerradura pueda ser adquirida. Lo que no es cierto en su caso, el locking está muy disputado por no menos de 24 hilos. Todo núcleo giratorio y ardiente sin tener la oportunidad de adquirir la cerradura.

En esta prueba, el Monitor funcionará mejor ya que pone en cola los hilos en espera para adquirir el locking. Se suspenden hasta que uno de ellos tenga la oportunidad de adquirir el locking, liberado de la cola de espera cuando se libera el locking. Dándoles a todos una oportunidad justa de tomar un turno, maximizando así las probabilidades de que todos terminen al mismo tiempo. Entrelazado. El aumento tampoco es malo, pero no puede proporcionar una garantía de imparcialidad.

Puede ser bastante difícil juzgar si Spinlock es el enfoque correcto desde el principio, hay que medirlo. Un analizador de concurrencia es el tipo correcto de herramienta.