Udostępnij za pośrednictwem


Instrukcje: używanie rozwiązania SpinLock do synchronizacji niskiego poziomu

W poniższym przykładzie pokazano, jak używać SpinLock. W tym przykładzie sekcja krytyczna wykonuje minimalną ilość pracy, co czyni ją dobrym kandydatem do SpinLock. Niewielki wzrost nakładu pracy powoduje zwiększenie wydajności SpinLock w porównaniu ze standardową blokadą. Jednak istnieje punkt, w którym SpinLock staje się droższy niż standardowa blokada. Możesz użyć funkcji profilowania współbieżności w narzędziach profilowania, aby zobaczyć, który typ blokady zapewnia lepszą wydajność w programie. Aby uzyskać więcej informacji, zobacz Concurrency Visualizer.


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 może być przydatne, gdy blokada zasobu udostępnionego nie będzie przechowywana przez bardzo długi czas. W takich przypadkach na komputerach wielordzeniowych może być wydajne, gdy zablokowany wątek kręci się przez kilka cykli, dopóki blokada nie zostanie zwolniona. Podczas kręcenia się wątek nie zostaje zablokowany, co jest procesem intensywnie obciążającym CPU. SpinLock przestaną się obracać w określonych warunkach, aby zapobiec zagłodzeniu procesorów logicznych lub inwersji priorytetów w systemach z funkcją Hyper-Threading.

W tym przykładzie użyto klasy System.Collections.Generic.Queue<T>, która wymaga synchronizacji użytkowników w celu uzyskania dostępu wielowątkowego. Inną opcją jest użycie System.Collections.Concurrent.ConcurrentQueue<T>, która nie wymaga żadnych blokad użytkownika.

Zwróć uwagę na użycie false w wywołaniu do SpinLock.Exit. Zapewnia to najlepszą wydajność. Określ true w architekturach IA64, aby używać bariery pamięci, co opróżnia bufory zapisu, aby upewnić się, że blokada jest teraz dostępna dla innych wątków.

Zobacz też