방법: SpinWait을 사용하여 2단계 대기 작업 구현
다음 예제는 System.Threading.SpinWait 개체를 사용하여 2단계 대기 작업을 구현하는 방법을 보여줍니다. 첫 번째 단계에서 동기화 개체(Latch
)는 잠금이 사용 가능해졌는지 여부를 확인하는 동안 몇 주기 동안 회전합니다. 두 번째 단계에서는 잠금이 사용 가능하게 되면 Wait
메서드에서 System.Threading.ManualResetEvent을 사용하여 대기를 수행하지 않고 반환합니다. 잠금이 사용 가능하지 않으면 Wait
에서 대기를 수행합니다.
예시
이 예제는 래치 동기화 기본 형식의 매우 기본적인 구현을 보여줍니다. 대기 시간이 매우 짧을 것으로 예상되는 경우 이 데이터 구조를 사용할 수 있습니다. 이 예제는 예시용입니다. 프로그램에 래치 유형 기능이 필요한 경우 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
SpinOnce
에 대한 다음 호출로 인해 SpinWait에서 스레드의 시간 조각을 생성할 때까지만 래치는 SpinWait 개체를 사용하여 적절히 회전합니다. 이 지점에서 래치는 ManualResetEvent에서 WaitOne을 호출하고 나머지 시간 제한 값을 전달하여 자체 컨텍스트 전환을 발생시킵니다.
로깅 출력은 ManualResetEvent를 사용하지 않고 잠금을 획득하여 래치가 성능을 향상시킬 수 있었던 빈도를 표시합니다.
참고 항목
.NET