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 для целевой переменной. В сценарии с интенсивной записью, подобном вашему, потребуется значительное количество повторных попыток, завершение каждой повторной попытки может генерировать значительный объем трафика на межъядерной шине для сохранения согласованности кэша. Использование монитора может как-то лучше "сериализовать" доступ, уменьшая трафик на шине между ядрами / между процессорами. Но все это только догадки:)

Другие вопросы по тегам