SpinLock в C#. В каком типе алгоритма SpinLock является лучшим выбором по сравнению с Monitor?
Возможный дубликат:
Почему все утверждают, что SpinLock быстрее?
Этот вопрос касается SpinLock, Monitor & Interlocked.
Я сделал 2 теста, которые проверяют производительность Monitor
, SpinLock
а также Interlocked
и эти испытания оставили меня в замешательстве.
Мое замешательство касается, в частности, насколько быстро SpinLock
на самом деле. По моим тестам SpinLock
медленнее, чем Monitor
, Но на основании ряда документов и статей SpinLock
должен обеспечить прирост производительности.
И теперь мне интересно, в каких сценариях SpinLock
дать улучшение производительности?
Ниже вы можете найти некоторые подробности о тестах, которые я провел:
В первом тесте я создал несколько потоков (столько же аппаратных потоков, сколько у меня есть), обращающихся к одному и тому же объекту разделяемой блокировки, выполняя очень короткую операцию (или вообще без операции: это всего лишь тест).
Во втором тесте я создал массив элементов и несколько потоков, произвольно обращающихся к элементам в этом массиве. Каждый элемент содержит свой собственный объект блокировки: System.Object
за Monitor
тестовое задание, SpinLock
объект для SpinLock
тест, как для Interlocked.Increment
, поток использует открытую переменную типа int внутри элемента массива для выполнения Interlocked.Increment
операция.
В каждом тесте доступ к совместно используемой области выполняется в цикле. Каждый тест состоял из 3 процедур:
- Тестирование SpinLock
- Монитор тестирования
- Тестирование Инкремент.
Каждый тест показал, что SpinLock
был медленнее, чем Monitor
, Итак, еще раз вопрос, который беспокоит меня с тех пор, как я провел упомянутые тесты, заключается в том, какие сценарии подходят для повышения производительности, предоставляемого SpinLock
Размещаем код тестов, чтобы сообщить подробности:
(Оба теста были скомпилированы для.net 4.5)
ТЕСТ 1. Потоки пытаются получить монопольный доступ к одному и тому же общему объекту блокировки.
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;
_threadsCount = (_threadsCount == 0) ? 1 : _threadsCount;
Console.WriteLine("Cores/processors count: {0}", Environment.ProcessorCount);
Console.WriteLine("Threads count: {0}", _threadsCount);
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;
}
}
ТЕСТ 2. Потоки пытаются получить эксклюзивный доступ к элементам массива, которые выбираются случайным образом, т. Е. Тестирование с низким уровнем конкуренции
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace TestConcurrency
{
class Program
{
static int _loopsCount = 10000000;
static int _threadsCount = -1;
static int _arrayCount = 1000;
static ProcessPriorityClass _processPriority = ProcessPriorityClass.RealTime;
static ThreadPriority _threadPriority = ThreadPriority.Highest;
static void Main(string[] args)
{
_threadsCount = Environment.ProcessorCount;
_threadsCount = (_threadsCount == 0) ? 1 : _threadsCount;
Console.WriteLine("Cores/processors count: {0}", Environment.ProcessorCount);
Console.WriteLine("Threads count: {0}", _threadsCount);
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 IEnumerable<int> newList()
{
return Enumerable.Range(0, _arrayCount);
}
static TimeSpan ExecuteMonitor()
{
ManualResetEvent _startEvent = new ManualResetEvent(false);
CountdownEvent _endCountdown = new CountdownEvent(_threadsCount);
Thread[] threads = new Thread[_threadsCount];
var array = newList().Select(i => new ArrayElementForMonitor()).ToArray();
for (int i = 0; i < threads.Length; i++)
{
int localI = i;
threads[i] = new Thread(() =>
{
Random r = new Random(localI * localI * localI);
int index = 0;
_startEvent.WaitOne();
bool lockTaken;
for (int j = 0; j < _loopsCount; j++)
{
index = r.Next(0, _arrayCount);
lockTaken = false;
try
{
Monitor.Enter(array[index].Locker, ref lockTaken);
}
finally
{
if (lockTaken)
{
Monitor.Exit(array[index].Locker);
}
}
}
_endCountdown.Signal();
});
threads[i].Priority = _threadPriority;
threads[i].Start();
}
GC.Collect();
Stopwatch sw = Stopwatch.StartNew();
_startEvent.Set();
_endCountdown.Wait();
return sw.Elapsed;
}
static TimeSpan ExecuteSpinLock()
{
ManualResetEvent _startEvent = new ManualResetEvent(false);
CountdownEvent _endCountdown = new CountdownEvent(_threadsCount);
Thread[] threads = new Thread[_threadsCount];
var array = newList().Select(i => new ArrayElementForSpinLock()).ToArray();
for (int i = 0; i < threads.Length; i++)
{
int localI = i;
threads[i] = new Thread(() =>
{
Random r = new Random(localI * localI * localI);
int index = 0;
_startEvent.WaitOne();
bool lockTaken;
for (int j = 0; j < _loopsCount; j++)
{
index = r.Next(0, _arrayCount);
lockTaken = false;
try
{
array[index].Locker.Enter(ref lockTaken);
}
finally
{
if (lockTaken)
{
array[index].Locker.Exit();
}
}
}
_endCountdown.Signal();
});
threads[i].Priority = _threadPriority;
threads[i].Start();
}
GC.Collect();
Stopwatch sw = Stopwatch.StartNew();
_startEvent.Set();
_endCountdown.Wait();
return sw.Elapsed;
}
static TimeSpan ExecuteInterlocked()
{
ManualResetEvent _startEvent = new ManualResetEvent(false);
CountdownEvent _endCountdown = new CountdownEvent(_threadsCount);
Thread[] threads = new Thread[_threadsCount];
var array = newList().Select(i => new ArrayElementInterlocked()).ToArray();
for (int i = 0; i < threads.Length; i++)
{
int localI = i;
threads[i] = new Thread(() =>
{
Random r = new Random(localI * localI * localI);
int index = 0;
_startEvent.WaitOne();
for (int j = 0; j < _loopsCount; j++)
{
index = r.Next(0, _arrayCount);
Interlocked.Increment(ref array[index].Element);
}
_endCountdown.Signal();
});
threads[i].Priority = _threadPriority;
threads[i].Start();
}
GC.Collect();
Stopwatch sw = Stopwatch.StartNew();
_startEvent.Set();
_endCountdown.Wait();
return sw.Elapsed;
}
}
public class ArrayElementForMonitor
{
public object Locker = new object();
}
public class ArrayElementForSpinLock
{
public SpinLock Locker = new SpinLock();
}
public class ArrayElementInterlocked
{
public int Element;
}
}
ДОПОЛНИТЕЛЬНЫЙ ТЕСТ 3. Тест выполняется в одном потоке. Наибольшие шансы потока получить доступ к замку.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace TestSimpleLocking
{
class Program
{
static int _loopsCount = 100000000;
static ProcessPriorityClass _processPriority = ProcessPriorityClass.RealTime;
static ThreadPriority _threadPriority = ThreadPriority.Highest;
static void Main(string[] args)
{
Process.GetCurrentProcess().PriorityClass = _processPriority;
Thread.CurrentThread.Priority = _threadPriority;
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 ExecuteMonitor()
{
object locker = new object();
int variable = 0;
Stopwatch sw = Stopwatch.StartNew();
bool lockTaken = false;
for (int i = 0; i < _loopsCount; i++)
{
lockTaken = false;
try
{
Monitor.Enter(locker, ref lockTaken);
variable++;
}
finally
{
if (lockTaken)
{
Monitor.Exit(locker);
}
}
}
sw.Stop();
Console.WriteLine(variable);
return sw.Elapsed;
}
static TimeSpan ExecuteSpinLock()
{
SpinLock spinLock = new SpinLock();
int variable = 0;
Stopwatch sw = Stopwatch.StartNew();
bool lockTaken = false;
for (int i = 0; i < _loopsCount; i++)
{
lockTaken = false;
try
{
spinLock.Enter(ref lockTaken);
variable++;
}
finally
{
if (lockTaken)
{
spinLock.Exit();
}
}
}
sw.Stop();
Console.WriteLine(variable);
return sw.Elapsed;
}
static TimeSpan ExecuteInterlocked()
{
int variable = 0;
Stopwatch sw = Stopwatch.StartNew();
for (int i = 0; i < _loopsCount; i++)
{
Interlocked.Increment(ref variable);
}
sw.Stop();
Console.WriteLine(variable);
return sw.Elapsed;
}
}
}
Насколько я понимаю, третий тест - лучший случай для SpinLock
выбор. Нет споров на всех. Одиночный поток - последовательное исполнение. Зачем SpinLock
все еще далеко позади Monitor
? Может ли кто-нибудь указать мне код, который доказал бы мне, что SpinLock
полезен вообще (кроме разработки драйвера устройства)?
1 ответ
SpinLock очень быстр, если конкуренция за ресурс низкая (т.е. когда блокировка ресурса почти всегда заканчивается успешно). Ссылка: книга и блог Джо Даффи http://www.bluebytesoftware.com/blog/
В каждом тесте доступ к общей области выполняется в цикле
_could_ означают, что разногласия высоки; (Кстати, можете ли вы опубликовать полный пример кода? Это помогло бы и сократить требуемые "догадки"). Поэтому вполне вероятно, что SpinLock вращается, а затем ждет, что делает его хуже, чем монитор, который ожидает напрямую.
РЕДАКТИРОВАТЬ: после прочтения деталей вашего закрытого, связанного вопроса: я полностью согласен с ответом Ханса Пассанта:
Итак, основные требования состоят в том, чтобы блокировка удерживалась в течение очень короткого времени, что справедливо в вашем случае. И что есть разумные шансы на то, что блокировка может быть получена. Что не так в вашем случае, блокировка сильно оспаривается не менее чем 24 потоками.
Слепое использование SpinLock без измерения и / или без понимания, по крайней мере, принципов, лежащих в основе его разработки, является случаем преждевременной оптимизации, которая может быстро привести к медленному или даже некорректному коду: помните, что некоторые структуры синхронизации гарантируют справедливость и / или прогресс, другие нет; некоторые работают лучше, когда большая часть доступа доступна только для чтения, другие, когда конкуренция низкая,.... И в этом случае справедливость может иметь значение.
Еще одна быстрая, непроверенная гипотеза: меня больше удивило, что InterlockedIncrement
медленнее или равно монитору. Это заставило меня задуматься о проблемах согласованности кэша; в конце концов, Interlocked также работает лучше всего, когда имеется небольшое количество конфликтов записи, потому что он реализует использование атомарных операций CAS для целевой переменной. В сценарии с интенсивной записью, подобном вашему, потребуется значительное количество повторных попыток, завершение каждой повторной попытки может генерировать значительный объем трафика на межъядерной шине для сохранения согласованности кэша. Использование монитора может как-то лучше "сериализовать" доступ, уменьшая трафик на шине между ядрами / между процессорами. Но все это только догадки:)