I'm uncertain which programming language you're using, but the following C# implementation is easily refactorable C++, Java, etc. - and should solve your problem if I understand it correctly. Find Benchmarks and the full implementation below the example section.
Example Usage
The following is a simple example of how to use the class. Instantiate and pass to all threads. Calls to CompareExchange and Exchange are atomic operations on the static 'long current' variable, which may be any stack based value type (e.g. struct) and has no size restrictions. A call to Cancel on any cancels all waits to Acquire across threads and throws AtomicCancellationException across threads to terminate and transition program flow to the catch blocks as illustrated in the example. See code comments for additional details:
//pass a reference to this class to all threads
public class AtomicExample
{
//static global variable being updated by all threads sequentially
static long current = 0;
//Instantiate Atomic<T> w/ desired struct type param
Atomic<long> _lock = new();
public bool Atomic_CompareExchange(long value)
{
try
{
//updates the value atomically if not equal to current
if (_lock.CompareExchange(ref current, value, current) == value)
return true; //current == comparand, current = value
}
catch (AtomicCancellationException)
{
//threads awaiting spinlock terminated, cleanup and shutdown
}
return false; //current != comarand, current = current
}
public long Atomic_Exchange(long value)
{
try
{
//updates the value atomically regardless of equality (i.e. CompareExchange above)
return _lock.Exchange(ref current, value);
}
catch (AtomicCancellationException)
{
//thread was terminated cleanup and shutdown
}
return current;
}
// 1. terminates all waits to Acquire lock
// 2. transitions program flow to the catch blocks above on all threads
public void Cancel()
{
_lock.Cancel();
}
}
Benchmark
The following are BenchmarkDotNet comparisons between Interlocked and the Atomic implementation below. All benchmarks are 1M iterations with 2 competing threads. InterLocked doesn't support types > 8-bytes, which is why there is no head-to-head comp for Guid. In addition, Interlocked is a static class that is not intended to solve cross thread signaling and cancellation like Atomic<T>
- "InterLocked_..." -
InterLocked.CompareExchange
- "Atomic..." -
Atomic<T>.CompareExchange - implementation below
- "Lock..." -
Atomic<T>.CompareExchange - modified to use lock{...}
| Method | Mean | Error | StdDev | Ratio | RatioSD |
|----------------- |----------:|----------:|----------:|------:|--------:|
| Interlocked_Long | 6.989 ms | 0.0541 ms | 0.0506 ms | 1.00 | 0.00 |
| Atomic_Long | 9.566 ms | 0.0858 ms | 0.0761 ms | 1.37 | 0.01 |
| Lock_Long | 19.020 ms | 0.0721 ms | 0.0563 ms | 2.72 | 0.02 |
| Atomic_Guid | 76.644 ms | 1.0858 ms | 1.1151 ms | 10.98 | 0.15 |
| Lock__Guid | 84.223 ms | 0.1813 ms | 0.1514 ms | 12.05 | 0.09 |
Implementation
[StructLayout(LayoutKind.Auto)]
/// <summary>Atomic<T> class supports attomic CompareExchange and Exchange opertaions
/// in and "Interlocked" thread-safe mannor supporting of any struct/value (stack based) type</summary>
public struct Atomic<T> where T : struct
{
private AtomicSpinWait _lock = new();
//constructor
public Atomic() {}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public T CompareExchange(ref T current, T value, T compareand)
{
_lock.Acquire();
var sizeOf = Unsafe.SizeOf<T>();
// Note: comparison of bytes with pointer implemented inside .Net's "CreateReadOnlySpan"
// use pinned byte pointer and replace with iterator for C, C+++
if (!MemoryMarshal.CreateReadOnlySpan(ref Unsafe.As<T, byte>(ref current), sizeOf).SequenceEqual(
MemoryMarshal.CreateReadOnlySpan(ref Unsafe.As<T, byte>(ref compareand), sizeOf)))
current = value;
_lock.Release();
return current;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public T Exchange(ref T location, T value)
{
_lock.Acquire();
location = value;
_lock.Release();
return location;
}
public void Cancel() => _lock.Cancel();
[StructLayout(LayoutKind.Auto)]
private struct AtomicSpinWait
{
private volatile int _value;
private volatile int _cancel = 0;
public AtomicSpinWait() => _value = 0;
// cancells all threads awaiting entry to acquire and throws AtomicCancellationException
internal void Acquire()
{
for (var sw = new SpinWait(); CompareExchange(1, 0) == 1 && _cancel == 0; sw.SpinOnce()) ;
if (_cancel == 1) throw new AtomicCancellationException();
}
internal void Release() => _value = 0;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private int CompareExchange(int value, int comparand)
=> Interlocked.CompareExchange(ref _value, value, comparand);
public void Cancel() => Interlocked.Exchange(ref _cancel, 1);
}
}
//Exception thrown terminating locks across threads waiting to Acquire() lock
public class AtomicCancellationException : Exception { }
//Refactor of Microsoft's SpinWait impl to make things simple
public struct SpinWait
{
internal static readonly bool IsSingleProcessor = Environment.ProcessorCount == 1;
internal static readonly int SpinCountforSpinBeforeWait = (IsSingleProcessor ? 1 : 35);
private int _count;
public int Count { get; internal set; }
public bool NextSpinWillYield
{
get
{
if (_count < 10)
{
return IsSingleProcessor;
}
return true;
}
}
public void SpinOnce()
{
SpinOnceCore(20);
}
public void SpinOnce(int sleep1Threshold)
{
if (sleep1Threshold < -1)
{
throw new ArgumentOutOfRangeException("sleep1Threshold: " + sleep1Threshold);
}
if (sleep1Threshold >= 0 && sleep1Threshold < 10)
{
sleep1Threshold = 10;
}
SpinOnceCore(sleep1Threshold);
}
private void SpinOnceCore(int sleep1Threshold)
{
if ((_count >= 10 && ((_count >= sleep1Threshold && sleep1Threshold >= 0) || (_count - 10) % 2 == 0)) || IsSingleProcessor)
{
if (_count >= sleep1Threshold && sleep1Threshold >= 0)
{
Thread.Sleep(1);
}
else
{
int num = ((_count >= 10) ? ((_count - 10) / 2) : _count);
if (num % 5 == 4)
{
Thread.Sleep(0);
}
else
{
Thread.Yield();
}
}
}
else
{
int num2 = 7;
if (_count <= 30 && 1 << _count < num2)
{
num2 = 1 << _count;
}
Thread.SpinWait(num2);
}
_count = ((_count == int.MaxValue) ? 10 : (_count + 1));
}
public void Reset()
{
_count = 0;
}
}