方法: SpinWait を使用して 2 フェーズ待機操作を実装する
System.Threading.SpinWait オブジェクトを使用して 2 フェーズ待機操作を実装する方法を次の例に示します。 最初のフェーズでは、同期オブジェクト Latch は、ロックが使用可能になったかどうかをチェックしながら数サイクルの間スピンします。 2 番目のフェーズでは、ロックが使用可能になると、System.Threading.ManualResetEvent を使用して待機を実行することなく、Wait メソッドから制御が戻ります。それ以外の場合は、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();
}
}
}
このラッチでは、SpinOnce を次に呼び出して SpinWait がスレッドのタイム スライスを譲渡することになるまでの間だけ、SpinWait オブジェクトを使用してその場でスピンします。 その時点で、ManualResetEvent で WaitOne(Int32, Boolean) を呼び出し、タイムアウト値の残りを渡すことによって、ラッチのコンテキスト スイッチが行われます。
ログ出力から、Latch が ManualResetEvent を使用せずにロックを取得することでどの程度パフォーマンスを向上させることができたかがわかります。