如何:使用 SpinWait 实现两阶段等待操作
下面的示例演示如何使用 System.Threading.SpinWait 对象实现两阶段等待操作。 在第一个阶段中,同步对象 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();
}
}
}
闩锁使用 SpinWait 对象就地旋转,直至下一个对 SpinOnce 的调用导致 SpinWait 让出线程的时间片。 此时,通过对 ManualResetEvent 调用 WaitOne(Int32, Boolean) 并递入超时值的剩余部分,闩锁会导致其自身的上下文切换。
日志输出显示了闩锁能够在不使用 ManualResetEvent 的情况下通过获取锁来提高性能的频率。