Comment : utiliser SpinWait pour implémenter une opération d'attente en deux phases

L’exemple suivant montre comment utiliser un objet System.Threading.SpinWait pour implémenter une opération d’attente en deux phases. Durant la première phase, l’objet de synchronisation (Latch) tourne pendant quelques cycles, le temps de vérifier si le verrou est disponible. Durant la deuxième phase, si le verrou est enfin disponible, la méthode Wait est retournée sans utiliser le System.Threading.ManualResetEvent pour exécuter son attente. Dans le cas contraire, Wait exécute l’attente.

Exemple

Cet exemple montre une implémentation de base d’une primitive de synchronisation de verrou. Vous pouvez utiliser cette structure de données quand les temps d’attente sont supposés être très courts. Cet exemple est fourni à des fins de démonstration uniquement. Si vous avez besoin d’une fonctionnalité de type verrou dans votre programme, optez pour l’utilisation de System.Threading.ManualResetEventSlim.

#define LOGGING

using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;

class Latch
{
   private object latchLock = new object();
   // 0 = unset, 1 = set.
   private int m_state = 0;
   private volatile int totalKernelWaits = 0;

   // Block threads waiting for ManualResetEvent.
   private ManualResetEvent m_ev = new ManualResetEvent(false);
#if LOGGING
   // For fast logging with minimal impact on latch behavior.
   // Spin counts greater than 20 might be encountered depending on machine config.
   private long[] spinCountLog = new long[20];

   public void DisplayLog()
   {
      for (int i = 0; i < spinCountLog.Length; i++)
      {
          Console.WriteLine("Wait succeeded with spin count of {0} on {1:N0} attempts",
                            i, spinCountLog[i]);
      }
      Console.WriteLine("Wait used the kernel event on {0:N0} attempts.", totalKernelWaits);
      Console.WriteLine("Logging complete");
   }
#endif

   public void Set()
   {
      lock(latchLock) {
         m_state = 1;
         m_ev.Set();
      }
   }

   public void Wait()
   {
      Trace.WriteLine("Wait timeout infinite");
      Wait(Timeout.Infinite);
   }

   public bool Wait(int timeout)
   {
      SpinWait spinner = new SpinWait();
      Stopwatch watch;

      while (m_state == 0)
      {
          // Lazily allocate and start stopwatch to track timeout.
          watch = Stopwatch.StartNew();

          // Spin only until the SpinWait is ready
          // to initiate its own context switch.
          if (!spinner.NextSpinWillYield)
          {
              spinner.SpinOnce();
          }
          // Rather than let SpinWait do a context switch now,
          //  we initiate the kernel Wait operation, because
          // we plan on doing this anyway.
          else
          {
              Interlocked.Increment(ref totalKernelWaits);
              // Account for elapsed time.
              long realTimeout = timeout - watch.ElapsedMilliseconds;

              // Do the wait.
              if (realTimeout <= 0 || !m_ev.WaitOne((int)realTimeout))
              {
                  Trace.WriteLine("wait timed out.");
                  return false;
              }
          }
      }

#if LOGGING
      Interlocked.Increment(ref spinCountLog[spinner.Count]);
#endif
      // Take the latch.
      Interlocked.Exchange(ref m_state, 0);

      return true;
   }
}

class Example
{
   static Latch latch = new Latch();
   static int count = 2;
   static CancellationTokenSource cts = new CancellationTokenSource();

   static void TestMethod()
   {
      while (!cts.IsCancellationRequested)
      {
         // Obtain the latch.
         if (latch.Wait(50))
         {
            // Do the work. Here we vary the workload a slight amount
            // to help cause varying spin counts in latch.
            double d = 0;
            if (count % 2 != 0) {
               d = Math.Sqrt(count);
            }
            Interlocked.Increment(ref count);

            // Release the latch.
            latch.Set();
         }
      }
   }

   static void Main()
   {
      // Demonstrate latch with a simple scenario: multiple
      // threads updating a shared integer. Both operations
      // are relatively fast, which enables the latch to
      // demonstrate successful waits by spinning only.
      latch.Set();

      // UI thread. Press 'c' to cancel the loop.
      Task.Factory.StartNew(() =>
      {
         Console.WriteLine("Press 'c' to cancel.");
         if (Console.ReadKey(true).KeyChar == 'c') {
            cts.Cancel();
         }
      });

      Parallel.Invoke( () => TestMethod(),
                       () => TestMethod(),
                       () => TestMethod() );

#if LOGGING
      latch.DisplayLog();
      if (cts != null) cts.Dispose();
#endif
   }
}
#Const LOGGING = 1

Imports System.Diagnostics
Imports System.Threading
Imports System.Threading.Tasks

Class Latch
    Private latchLock As New Object()
    ' 0 = unset, 1 = set.
    Private m_state As Integer = 0
    Private totalKernelWaits As Integer = 0

    ' Block threads waiting for ManualResetEvent.
    Private m_ev = New ManualResetEvent(False)

#If LOGGING Then
    ' For fast logging with minimal impact on latch behavior.
    ' Spin counts greater than 20 might be encountered depending on machine config.
    Dim spinCountLog(19) As Long

    Public Sub DisplayLog()
        For i As Integer = 0 To spinCountLog.Length - 1
            Console.WriteLine("Wait succeeded with spin count of {0} on {1:N0} attempts",
                              i, spinCountLog(i))
        Next
        Console.WriteLine("Wait used the kernel event on {0:N0} attempts.",
                          totalKernelWaits)
        Console.WriteLine("Logging complete")
    End Sub
#End If

    Public Sub SetLatch()
        SyncLock (latchLock)
            m_state = 1
            m_ev.Set()
        End SyncLock
    End Sub

    Public Sub Wait()
        Trace.WriteLine("Wait timeout infinite")
        Wait(Timeout.Infinite)
    End Sub

    Public Function Wait(ByVal timeout As Integer) As Boolean
        ' Allocated on the stack.
        Dim spinner = New SpinWait()
        Dim watch As Stopwatch

        While (m_state = 0)
            ' Lazily allocate and start stopwatch to track timeout.
            watch = Stopwatch.StartNew()

            ' Spin only until the SpinWait is ready
            ' to initiate its own context switch.
            If Not spinner.NextSpinWillYield Then
                spinner.SpinOnce()

                ' Rather than let SpinWait do a context switch now,
                '  we initiate the kernel Wait operation, because
                ' we plan on doing this anyway.
            Else
                Interlocked.Increment(totalKernelWaits)
                ' Account for elapsed time.
                Dim realTimeout As Long = timeout - watch.ElapsedMilliseconds

                ' Do the wait.
                If realTimeout <= 0 OrElse Not m_ev.WaitOne(realTimeout) Then
                    Trace.WriteLine("wait timed out.")
                    Return False
                End If
            End If
        End While

#If LOGGING Then
        Interlocked.Increment(spinCountLog(spinner.Count))
#End If
        ' Take the latch.
        Interlocked.Exchange(m_state, 0)

        Return True
    End Function
End Class

Class Program
    Shared latch = New Latch()
    Shared count As Integer = 2
    Shared cts = New CancellationTokenSource()
    Shared lockObj As New Object()

    Shared Sub TestMethod()
        While (Not cts.IsCancellationRequested)
            ' Obtain the latch.
            If (latch.Wait(50)) Then
                ' Do the work. Here we vary the workload a slight amount
                ' to help cause varying spin counts in latch.
                Dim d As Double = 0
                If (count Mod 2 <> 0) Then
                    d = Math.Sqrt(count)
                End If

                SyncLock (lockObj)
                    If count = Int32.MaxValue Then count = 0
                    count += 1
                End SyncLock

                ' Release the latch.
                latch.SetLatch()
            End If
        End While
    End Sub

    Shared Sub Main()
        ' Demonstrate latch with a simple scenario:
        ' two threads updating a shared integer and
        ' accessing a shared StringBuilder. Both operations
        ' are relatively fast, which enables the latch to
        ' demonstrate successful waits by spinning only. 
        latch.SetLatch()

        ' UI thread. Press 'c' to cancel the loop.
        Task.Factory.StartNew(Sub()
                                  Console.WriteLine("Press 'c' to cancel.")
                                  If (Console.ReadKey(True).KeyChar = "c"c) Then
                                      cts.Cancel()
                                  End If
                              End Sub)
        Parallel.Invoke(
               Sub() TestMethod(),
               Sub() TestMethod(),
               Sub() TestMethod()
               )

#If LOGGING Then
        latch.DisplayLog()
#End If
        If cts IsNot Nothing Then cts.Dispose()
    End Sub
End Class

Le verrou utilise l’objet SpinWait pour tourner sur place uniquement jusqu’à ce que le prochain appel à SpinOnce provoque la transmission de la tranche horaire du thread par le SpinWait. À ce stade, le verrou provoque son propre changement de contexte en appelant WaitOne sur le ManualResetEvent et en passant le reste de la valeur de délai d’attente.

La sortie de la journalisation indique la fréquence à laquelle le verrou a pu augmenter les performances en acquérant le verrou sans utiliser ManualResetEvent.

Voir aussi