Condividi tramite


Procedura: utilizzare SpinWait per implementare un'operazione di attesa a due fasi

Nell'esempio seguente viene illustrato come utilizzare un oggetto System.Threading.SpinWait per implementare un'operazione di attesa a due fasi. Nella prima fase l'oggetto di sincronizzazione, un oggetto Latch, esegue uno spin per alcuni cicli mentre verifica la disponibilità del blocco. Nella seconda fase, se il blocco diventa disponibile, il metodo Wait viene restituito senza utilizzare System.Threading.ManualResetEvent per eseguirne l'attesa; in caso contrario, Wait esegue l'attesa.

Esempio

In questo esempio viene illustrata un'implementazione molto semplice di una primitiva di sincronizzazione latch. È possibile utilizzare questa struttura dei dati quando si prevedono tempi di attesa molto brevi. Questo esempio viene riportato a scopo puramente dimostrativo. Se è necessaria una funzionalità di tipo latch nel programma, considerare l'utilizzo di System.Threading.ManualResetEventSlim.

#Const LOGGING = 1

Imports System
Imports System.Collections.Generic
Imports System.Diagnostics
Imports System.Linq
Imports System.Text
Imports System.Threading
Imports System.Threading.Tasks

Namespace CDS_Spinwait

    Class Latch

        ' 0 = unset, 1 = set
        Private m_state As Integer = 0

        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 As Integer()

        Private totalKernelWaits As Integer = 0
        Public Sub New()
            ReDim spinCountLog(19)
        End Sub

        Public Sub PrintLog()

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

        Public Sub SetLatch()

            ' Trace.WriteLine("Setlatch")
            Interlocked.Exchange(m_state, 1)
            m_ev.Set()
        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 (spinner.NextSpinWillYield = False) 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
#If LOGGING Then
                    Interlocked.Increment(totalKernelWaits)
#End If
                    ' Account for elapsed time.
                    Dim realTimeout As Long = timeout - watch.ElapsedMilliseconds

                    Debug.Assert(realTimeout <= Integer.MaxValue)
                    ' Do the wait.
                    If (realTimeout <= 0) Then

                        Trace.WriteLine("wait timed out.")
                        Return False
                    ElseIf m_ev.WaitOne(realTimeout) = False Then
                        Return False
                    End If

                End If

            End While

            ' Take the latch.

            Interlocked.Exchange(m_state, 0)


#If LOGGING Then
            Interlocked.Increment(spinCountLog(spinner.Count))
#End If


            Return True
        End Function
    End Class

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

        Shared Sub TestMethod()

            While (cts.IsCancellationRequested = False And count < Integer.MaxValue - 1)

                ' 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
                    Interlocked.Increment(count)

                    ' 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("Wait a few seconds, then press 'c' to see results.")
                                      If (Console.ReadKey().KeyChar = "c"c) Then
                                          cts.Cancel()
                                      End If
                                  End Sub)

            Parallel.Invoke(
                Sub() TestMethod(),
               Sub() TestMethod(),
                Sub() TestMethod()
                )

#If LOGGING Then
            latch.PrintLog()
#End If
            Console.WriteLine(vbCrLf & "To exit, press the Enter key.")
            Console.ReadLine()
        End Sub
    End Class
End Namespace
#define LOGGING

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace CDS_Spinwait
{
    class Latch
    {
        // 0 = unset, 1 = set
        private volatile int m_state = 0;

        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 int[] spinCountLog = new int[20];
        private volatile int totalKernelWaits = 0;

        public void PrintLog()
        {

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

        public void Set()
        {
            // Trace.WriteLine("Set");
            m_state = 1;
            m_ev.Set();
        }

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

        public bool Wait(int timeout)
        {
            // Allocated on the stack.
            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
                {
                    totalKernelWaits++;
                    // Account for elapsed time.
                    int realTimeout = timeout - (int)watch.ElapsedMilliseconds;

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

            // Take the latch.
            m_state = 0;
            //   totalWaits++;

#if LOGGING
            spinCountLog[spinner.Count]++;
#endif


            return true;
        }
    }

    class Program
    {
        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);
                    }
                    count++;

                    // Release the latch.
                    latch.Set();
                }
            }
        }
        static void Main(string[] args)
        {
            // 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.Set();


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

                }
            });

            Parallel.Invoke(

                () => TestMethod(),
                () => TestMethod(),
                () => TestMethod()
                );

#if LOGGING
            latch.PrintLog();
#endif
            Console.WriteLine("\r\nPress the Enter Key.");
            Console.ReadLine();
        }
    }
}

Il latch utilizza l'oggetto SpinWait per eseguire uno spin sul posto soltanto finché la chiamata successiva a SpinOnce non fa sì che SpinWait restituisca la porzione di tempo del thread. A quel punto, il latch provoca il proprio cambio di contesto chiamando WaitOne(Int32, Boolean) su ManualResetEvent e passando la parte restante del valore di timeout.

L'output di registrazione mostra la frequenza con cui il latch è stato in grado di aumentare le prestazioni acquisendo il blocco senza utilizzare ManualResetEvent.

Vedere anche

Altre risorse

SpinWait

Oggetti e funzionalità del threading