Поделиться через


Практическое руководство. Использование SpinLock для низкоуровневой синхронизации

В следующем примере показано, как использовать объект SpinLock. В этом примере критически важный раздел выполняет минимальный объем работы, что делает его хорошим кандидатом на работу SpinLock. Небольшое увеличение объема работы повышает производительность SpinLock по сравнению со стандартной блокировкой. Однако есть точка, в которой SpinLock становится дороже, чем стандартная блокировка. Функции профилирования параллелизма можно использовать в средствах профилирования, чтобы узнать, какой тип блокировки обеспечивает лучшую производительность в программе. Дополнительные сведения см. в разделе Визуализатор параллелизма.


class SpinLockDemo2
{
    const int N = 100000;
    static Queue<Data> _queue = new Queue<Data>();
    static object _lock = new Object();
    static SpinLock _spinlock = new SpinLock();

    class Data
    {
        public string Name { get; set; }
        public double Number { get; set; }
    }
    static void Main(string[] args)
    {

        // First use a standard lock for comparison purposes.
        UseLock();
        _queue.Clear();
        UseSpinLock();

        Console.WriteLine("Press a key");
        Console.ReadKey();
    }

    private static void UpdateWithSpinLock(Data d, int i)
    {
        bool lockTaken = false;
        try
        {
            _spinlock.Enter(ref lockTaken);
            _queue.Enqueue( d );
        }
        finally
        {
            if (lockTaken) _spinlock.Exit(false);
        }
    }

    private static void UseSpinLock()
    {

          Stopwatch sw = Stopwatch.StartNew();

          Parallel.Invoke(
                  () => {
                      for (int i = 0; i < N; i++)
                      {
                          UpdateWithSpinLock(new Data() { Name = i.ToString(), Number = i }, i);
                      }
                  },
                  () => {
                      for (int i = 0; i < N; i++)
                      {
                          UpdateWithSpinLock(new Data() { Name = i.ToString(), Number = i }, i);
                      }
                  }
              );
          sw.Stop();
          Console.WriteLine($"elapsed ms with spinlock: {sw.ElapsedMilliseconds}");
    }

    static void UpdateWithLock(Data d, int i)
    {
        lock (_lock)
        {
            _queue.Enqueue(d);
        }
    }

    private static void UseLock()
    {
        Stopwatch sw = Stopwatch.StartNew();

        Parallel.Invoke(
                () => {
                    for (int i = 0; i < N; i++)
                    {
                        UpdateWithLock(new Data() { Name = i.ToString(), Number = i }, i);
                    }
                },
                () => {
                    for (int i = 0; i < N; i++)
                    {
                        UpdateWithLock(new Data() { Name = i.ToString(), Number = i }, i);
                    }
                }
            );
        sw.Stop();
        Console.WriteLine($"elapsed ms with lock: {sw.ElapsedMilliseconds}");
    }
}
Imports System.Threading
Imports System.Threading.Tasks

Class SpinLockDemo2

    Const N As Integer = 100000
    Shared _queue = New Queue(Of Data)()
    Shared _lock = New Object()
    Shared _spinlock = New SpinLock()

    Class Data
        Public Name As String
        Public Number As Double
    End Class
    Shared Sub Main()

        ' First use a standard lock for comparison purposes.
        UseLock()
        _queue.Clear()
        UseSpinLock()

        Console.WriteLine("Press a key")
        Console.ReadKey()

    End Sub

    Private Shared Sub UpdateWithSpinLock(ByVal d As Data, ByVal i As Integer)

        Dim lockTaken As Boolean = False
        Try
            _spinlock.Enter(lockTaken)
            _queue.Enqueue(d)
        Finally

            If lockTaken Then
                _spinlock.Exit(False)
            End If
        End Try
    End Sub

    Private Shared Sub UseSpinLock()


        Dim sw = Stopwatch.StartNew()

        Parallel.Invoke(
               Sub()
                   For i As Integer = 0 To N - 1
                       UpdateWithSpinLock(New Data() With {.Name = i.ToString(), .Number = i}, i)
                   Next
               End Sub,
                Sub()
                    For i As Integer = 0 To N - 1
                        UpdateWithSpinLock(New Data() With {.Name = i.ToString(), .Number = i}, i)
                    Next
                End Sub
            )
        sw.Stop()
        Console.WriteLine("elapsed ms with spinlock: {0}", sw.ElapsedMilliseconds)
    End Sub

    Shared Sub UpdateWithLock(ByVal d As Data, ByVal i As Integer)

        SyncLock (_lock)
            _queue.Enqueue(d)
        End SyncLock
    End Sub

    Private Shared Sub UseLock()

        Dim sw = Stopwatch.StartNew()

        Parallel.Invoke(
                Sub()
                    For i As Integer = 0 To N - 1
                        UpdateWithLock(New Data() With {.Name = i.ToString(), .Number = i}, i)
                    Next
                End Sub,
               Sub()
                   For i As Integer = 0 To N - 1
                       UpdateWithLock(New Data() With {.Name = i.ToString(), .Number = i}, i)
                   Next
               End Sub
                )
        sw.Stop()
        Console.WriteLine("elapsed ms with lock: {0}", sw.ElapsedMilliseconds)
    End Sub
End Class

SpinLock может оказаться полезным, если блокировка общего ресурса не будет удерживаться в течение очень долгого времени. В таких случаях на многопроцессорных компьютерах для блокированного потока может оказаться эффективным вращаться в течение нескольких циклов, пока блокировка не будет снята. При спиннинге поток не блокируется, что является интенсивным процессом ЦП. SpinLock перестанет вращаться при определенных условиях, чтобы предотвратить голодание логических процессоров или инверсию приоритетов в системах с Hyper-Threading.

В этом примере используется класс System.Collections.Generic.Queue<T>, который требует синхронизации пользователем для многопотокового доступа. Другим вариантом является использование System.Collections.Concurrent.ConcurrentQueue<T>, которое не требует блокировки пользователей.

Обратите внимание на использование false в вызове SpinLock.Exit. Это обеспечивает лучшую производительность. Укажите true для архитектур IA64, чтобы использовать барьер памяти, который очищает буферы записи, обеспечивая тем самым доступность блокировки для других потоков.

См. также