Thread safe generic alternative to Interlocked.Exchange and Interlocked.Exchange global struct support

Viewed 91

Does `Interlocked.MemoryBarrier' provide sufficient fencing to generic type support as implemented in the following example?

    public static T CompareExchange<T>(ref T location, T value, T comparand)
        where T : unmanaged
    {
        Interlocked.MemoryBarrier();
        if (Unsafe.AreSame(ref location, ref comparand))
            return Exchange(ref location, value);
        return location;
    }

    public static T Exchange<T>(ref T location, T value)
        where T : unmanaged
    {
        Interlocked.MemoryBarrier();
        location = value;
        Interlocked.MemoryBarrier();
        return location;
    }
1 Answers

In your implementation Unsafe.AreSame() simple checks the reference to the same memory location. It isn't comparing the the struct for equality as seems to be your intent (otherwise just use the built in generic overload for reference types).

Regarding Interlocked.MemoryBarrier(), even if it does guarantee a full fence your implementation does not achieve atomicity for reasons pointed out by @PeterCordes and I in the comments section below. In your example, two concurrent calls to your implementation of Exchange could return the same initial value, which would be incorrect.

GLOBAL STRUCT SUPPORT ALTERNATIVE to Interlocked W/O "LOCK"

The following is a quick and dirty example of a CompareExchange implementation with global struct support (i.e. no 8-byte length restriction) that's marginally (microseconds per op) slower than Interlocked.Exchange() in a head to head test of 1M long RMW operations.

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.

  • "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)]
public struct Atomic<T> where T : struct
{
    private AtomicSpinWait _lock;
    public Atomic() => _lock = new();

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public T CompareExchange(ref T current, T value, T compareand)
    {
        _lock.Acquire();

        var sizeOf = Unsafe.SizeOf<T>();

        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;
    }

    [StructLayout(LayoutKind.Auto)]
    private struct AtomicSpinWait
    {
        private int _value;

        public AtomicSpinWait() => _value = 0;

        internal void Acquire()
        {
            for (var sw = new SpinWait(); CompareExchange(1, 0) == 1; sw.SpinOnce()) ;
        }

        internal void Release() => _value = 0;

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private int CompareExchange(int value, int comparand)
            => Interlocked.CompareExchange(ref _value, value, comparand);
    }

}

Example Usage


    public class AtomicExample
    {
        static long current = 0;
        
        //Instantiate Atomic<T> w/ desired struct type param       
        Atomic<long> _lock = new();
        public bool Example(long value, long comparand)
        {
           if (_lock.CompareExchange(ref current, value, comparand) == value)
               return true; //current == comparand, current = value
           return false; //current != comarand, current = current 
        }
    }
Related