Note

Access to this page requires authorization. You can try signing in or .

Access to this page requires authorization. You can try .

How to: Use SpinWait to Implement a Two-Phase Wait Operation

The following example shows how to use a System.Threading.SpinWait object to implement a two-phase wait operation. In the first phase, the synchronization object, a Latch, spins for a few cycles while it checks whether the lock has become available. In the second phase, if the lock becomes available, then the Wait method returns without using the System.Threading.ManualResetEvent to perform its wait; otherwise, Wait performs the wait.

Example

This example shows a very basic implementation of a Latch synchronization primitive. You can use this data structure when wait times are expected to be very short. This example is for demonstration purposes only. If you require latch-type functionality in your program, consider using 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 {i} on {spinCountLog[i]:N0} attempts");
 }
 Console.WriteLine($"Wait used the kernel event on {totalKernelWaits:N0} attempts.");
 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

The latch uses the SpinWait object to spin in place only until the next call to SpinOnce causes the SpinWait to yield the time slice of the thread. At that point, the latch causes its own context switch by calling WaitOne on the ManualResetEvent and passing in the remainder of the time-out value.

The logging output shows how often the Latch was able to increase performance by acquiring the lock without using the ManualResetEvent.

See also


Feedback

Was this page helpful?

Additional resources