Home > Back-end >  Thread safe generic alternative to Interlocked.Exchange and Interlocked.Exchange global struct suppo
Thread safe generic alternative to Interlocked.Exchange and Interlocked.Exchange global struct suppo

Time:09-17

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

// Out of scope Interlocked.* methods omitted

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

CodePudding user response:

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 this implementation of Exchange could return the same initial value, which would not be correct.

In order to achieve atomicity your limited to structs of 8-byte max size. The following would achieve your object under the 8-byte constraint:


public static T CompareExchange<T>(ref T location, T value, T comparand)
    where T : unmanaged =>
Unsafe.SizeOf<T>() switch
{
    4 => Unsafe.As<int, T>(ref Unsafe.AsRef(Interlocked.CompareExchange(ref Unsafe.As<T, int>(ref location), Unsafe.As<T, int>(ref value), Unsafe.As<T, int>(ref comparand)))),
    8 => Unsafe.As<long, T>(ref Unsafe.AsRef(Interlocked.CompareExchange(ref Unsafe.As<T, long>(ref location), Unsafe.As<T, long>(ref value), Unsafe.As<T, long>(ref comparand)))),
    _ => throw new NotSupportedException("Type exceeds 8-bytes")
};

public static T Exchange<T>(ref T location, T value)
    where T : unmanaged =>
Unsafe.SizeOf<T>() switch
{
    4 => Unsafe.As<int, T>(ref Unsafe.AsRef(Interlocked.Exchange(ref Unsafe.As<T, int>(ref location), Unsafe.As<T, int>(ref value)))),
    8 => Unsafe.As<long, T>(ref Unsafe.AsRef(Interlocked.Exchange(ref Unsafe.As<T, long>(ref location), Unsafe.As<T, long>(ref value)))),
    _ => throw new NotSupportedException("Type exceeds 8-bytes")
};

GLOBAL STRUCT SUPPORT ALTERNATIVE W/O USING "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 _barrier;
        public Atomic()
        {
            _barrier = new();
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public T CompareExchange(ref T current, T value, T compareand)
        {
            _barrier.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;
            
            _barrier.Release();

            return current;
        }
        
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public T Exchange(ref T location, T value)
        {
            _barrier.Acquire();

            location = value;

            _barrier.Release();

            return location;
        }

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

            public AtomicSpinWait() => _value = 0;

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

            internal void Release() => _value = 0;

            [MethodImpl(MethodImplOptions.AggressiveInlining)]
            public int CompareExchange(int update, int expected)
                => Interlocked.CompareExchange(ref _value, update, expected);
        }

    }

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