Como usar SpinWait para implementar uma operação bifásica de espera
O exemplo a seguir mostra como usar um objeto System.Threading.SpinWait para implementar uma operação de espera de duas fases. Na primeira fase, o objeto de sincronização, um Latch
, gira por alguns ciclos enquanto verifica se o bloqueio ficou disponível. Na segunda fase, se o bloqueio ficar disponível, o método Wait
retorna sem usar System.Threading.ManualResetEvent para executar sua espera; caso contrário, Wait
executa a espera.
Exemplo
Este exemplo mostra uma implementação muito básica de uma primitivo de sincronização de Trava. Você pode usar essa estrutura de dados quando houver a expectativa de tempos de espera muito curtos. Este exemplo é para fins de demonstração. Se você precisar de funcionalidade do tipo trava em seu programa, considere o uso de 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
A trava usa o objeto SpinWait para executar somente no local até que a próxima chamada a SpinOnce
faça com que SpinWait gere a fração de tempo do thread. Nesse ponto, a trava causa sua própria alternância de contexto chamando WaitOne no ManualResetEvent, e passando o restante do valor de tempo limite.
A saída do log mostra com que frequência a trava conseguiu aumentar o desempenho adquirindo o bloqueio sem usar o ManualResetEvent.