HOW TO:使用 SpinWait 實作兩階段等候作業

下列範例將示範如何使用 System.Threading.SpinWait 物件來實作兩階段等候作業。 在第一個階段中,當同步處理物件 Latch 檢查鎖定是否變成可用時,它會空轉少數循環。 在第二個階段中,如果鎖定變成可用,Wait 方法就會傳回而不使用 System.Threading.ManualResetEvent 來執行其等候。否則,Wait 就會執行等候。

範例

這個範例會示範 Latch 同步處理原始物件非常基本的實作。 當等候時間應該非常短暫時,您就可以使用這種資料結構。 這個範例僅供示範使用。 如果您需要在程式中使用閂鎖類型功能,請考慮使用 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();
        }
    }
}

此閂鎖只會使用 SpinWait 物件來就地空轉,直到下一次呼叫 SpinOnceSpinWait 產生執行緒的時間量為止。 此時,閂鎖就會針對 ManualResetEvent 呼叫 WaitOne(Int32, Boolean) 並傳入逾時值的餘數,藉以進行自訂內容切換。

記錄輸出會顯示 Latch 能夠透過取得鎖定而不使用 ManualResetEvent 來提升效能的頻率。

請參閱

其他資源

SpinWait

執行緒處理物件和功能