방법: SpinWait을 사용하여 2단계 대기 작업 구현
다음 예제에서는 System.Threading.SpinWait 개체를 사용하여 2단계 대기 작업을 구현하는 방법을 보여 줍니다. 첫 번째 단계에서는 동기화 개체인 Latch가 잠금을 사용할 수 있게 되었는지 여부를 확인하는 동안 몇 차례 회전합니다. 두 번째 단계에서는 잠금을 사용할 수 있으면 Wait 메서드가 System.Threading.ManualResetEvent를 사용하여 대기를 수행하지 않은 채 반환되고, 그렇지 않으면 Wait가 대기를 수행합니다.
예제
이 예제에서는 래치 동기화 기본 형식의 기본 구현을 보여 줍니다. 대기 시간이 매우 짧을 것으로 예상될 경우 이 데이터 구조를 사용할 수 있습니다. 이 예제는 예시 목적으로만 제공됩니다. 프로그램에 래치 형식 기능이 필요하면 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();
}
}
}
래치는 SpinOnce에 대한 다음 호출에 의해 SpinWait가 스레드의 시간 간격을 계산할 때까지만 SpinWait 개체를 사용하여 제자리에서 회전합니다. 이때 래치는 ManualResetEvent에 대해 WaitOne(Int32, Boolean)을 호출하고 제한 시간 값의 남아 있는 시간에 전달하여 컨텍스트 전환이 수행되도록 합니다.
로깅 결과에서는 래치가 ManualResetEvent를 사용하지 않고도 얼마나 자주 잠금을 획득하여 성능을 향상시킬 수 있었는지를 보여 줍니다.