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


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

В следующем примере демонстрируется использование примитива SpinLock.

Пример

В этом примере критический раздел выполняет минимальный объем работ, что подразумевает его эффективную работу с SpinLock. Увеличивая объем работы, небольшой объект повышает производительность SpinLock в сравнении со стандартной блокировкой. Однако при этом примитив SpinLock более ресурсоемкий в отличие от стандартной блокировки. С помощью новой возможности профилирования с параллелизмом в Средства профилирования Visual Studio Team Developer Edition можно определить, какой тип блокировки обеспечивает лучшую производительность программы. Дополнительные сведения см. в разделе Визуализатор параллелизма.

Imports System
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

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: {0}", 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: {0}", sw.ElapsedMilliseconds);
    }
}

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

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

Обратите внимание на использование false (False в Visual Basic) при вызове Exit. Это обеспечивает наилучшую производительность. Для архитектуры IA64 задайте значение true (True), чтобы воспользоваться барьером памяти, который очищает буферы записи, обеспечивая таким образом доступность блокировки для завершения работы других потоков.

См. также

Другие ресурсы

Объекты и функциональные возможности работы с потоками