C#联锁交换

Mar*_*tin 7 c# interlocked

我的游戏有点像这样:

public static float Time;

float someValue = 123;
Interlocked.Exchange(ref Time, someValue);
Run Code Online (Sandbox Code Playgroud)

我想把时间变成Uint32; 但是,当我尝试使用UInt32而不是使用float值时,它会抗议类型必须是引用类型.Float不是引用类型,因此我知道在技术上可以使用非引用类型执行此操作.是否有任何实用的方法来使这项工作UInt32

Jon*_*eet 17

有一个过载Interlocked.Exchange专为float(和其他人double,int,long,IntPtrobject).没有一个用于uint,因此编译器认为最接近的匹配是泛型Interlocked.Exchange<T>- 但在这种情况下T必须是引用类型.uint不是引用类型,因此也不起作用 - 因此错误消息.

换一种说法:

至于做什么,选项是以下任何一个:

  • int正如马克所暗示的那样,可能会使用它.
  • 如果您需要额外的范围,请考虑使用long.
  • 使用uint但不要尝试编写无锁代码

虽然显然Exchange可以使用某些特定的值类型,但Microsoft并未针对所有基本类型实现它.我无法想象这样做会很难(毕竟它们只是位),但可能他们想要保持过载数量下降.


Gle*_*den 16

虽然难看,它实际上可能执行的原子交换CompareExchange上枚举或64个比特或更少使用其他blittable值类型unsafeC#代码:

enum MyEnum { A, B, C };

MyEnum m_e = MyEnum.B;

unsafe void example()
{
    MyEnum e = m_e;
    fixed (MyEnum* ps = &m_e)
        if (Interlocked.CompareExchange(ref *(int*)ps, (int)(e | MyEnum.C), (int)e) == (int)e)
        {
            /// change accepted, m_e == B | C
        }
        else
        {
            /// change rejected
        }
}
Run Code Online (Sandbox Code Playgroud)

违反直觉的部分是解除引用指针上的ref表达式确实实际渗透到枚举的地址.我认为编译器在其权限范围内已经在堆栈上生成了一个不可见的临时变量,在这种情况下这不起作用.使用风险由您自己承担.

[编辑:针对OP请求的特定类型]

static unsafe uint CompareExchange(ref uint target, uint v, uint cmp)
{
    fixed (uint* p = &target)
        return (uint)Interlocked.CompareExchange(ref *(int*)p, (int)v, (int)cmp);
}
Run Code Online (Sandbox Code Playgroud)

[编辑:和64位无符号长]

static unsafe ulong CompareExchange(ref ulong target, ulong v, ulong cmp)
{
    fixed (ulong* p = &target)
        return (ulong)Interlocked.CompareExchange(ref *(long*)p, (long)v, (long)cmp);
}
Run Code Online (Sandbox Code Playgroud)

(我也尝试使用未记录的C#关键字__makeref来实现这一点,但是这不起作用,因为你不能ref在dreferenced上使用__refvalue.这太糟糕了,因为CLR将InterlockedExchange函数映射到一个私有的内部函数,该函数在TypedReference [评论中提出]通过JIT拦截,见下文])


[编辑:2018年7月]您现在可以使用System.Runtime.CompilerServices更加高效地执行此操作.不安全的库包.您的方法可以Unsafe.As<TFrom,TTo>()用来直接重新解释目标托管引用引用的类型,避免固定和转换到unsafe模式的双重费用:

static uint CompareExchange(ref uint target, uint value, uint expected) =>
    (uint)Interlocked.CompareExchange(
                            ref Unsafe.As<uint, int>(ref target),
                            (int)value,
                            (int)expected);

static ulong CompareExchange(ref ulong target, ulong value, ulong expected) =>
    (ulong)Interlocked.CompareExchange(
                            ref Unsafe.As<ulong, long>(ref target),
                            (long)value,
                            (long)expected);
Run Code Online (Sandbox Code Playgroud)

当然这也适用Interlocked.Exchange.以下是4字节和8字节无符号类型的助手.

static uint Exchange(ref uint target, uint value) =>
    (uint)Interlocked.Exchange(ref Unsafe.As<uint, int>(ref target), (int)value);

static ulong Exchange(ref ulong target, ulong value) =>
    (ulong)Interlocked.Exchange(ref Unsafe.As<ulong, long>(ref target), (long)value);
Run Code Online (Sandbox Code Playgroud)

这也适用于枚举类型 - 但只要它们的基础原始整数恰好是四个或八个字节.换句话说,int(32位)或long(64位)大小.限制是这些是Interlocked.CompareExchange过载中唯一的两个位宽.默认情况下,在没有指定基础类型时enum使用int,因此MyEnum(从上面)工作正常.

static MyEnum CompareExchange(ref MyEnum target, MyEnum value, MyEnum expected) =>
    (MyEnum)Interlocked.CompareExchange(
                            ref Unsafe.As<MyEnum, int>(ref target),
                            (int)value,
                            (int)expected);

static MyEnum Exchange(ref MyEnum target, MyEnum value) =>
    (MyEnum)Interlocked.Exchange(ref Unsafe.As<MyEnum, int>(ref target), (int)value);
Run Code Online (Sandbox Code Playgroud)

我不知道的4个字节的最小值是否是.NET的根本,但据我可以告诉它留下的原子交换较小的8位或16位的基本类型((值)没有办法byte,sbyte,char,ushort,short)没有冒险对相邻字节造成附带损害的风险.在以下示例中,BadEnum显式指定的大小太小而无法进行原子交换,而不会影响最多三个相邻字节.

enum BadEnum : byte { };    // can't swap less than 4 bytes on .NET?
Run Code Online (Sandbox Code Playgroud)

如果您不受互操作(或其他固定)布局的约束,则解决方法是确保此类枚举的内存布局始终填充到最小4字节以允许原子交换(as int).然而,似乎这样做可能会破坏首先指定较小​​宽度的任何目的.



[编辑:2017年4月]我最近了解到,当.NET以32位模式运行时(或者,即在WOW子系统中),64位Interlocked操作不能保证在Interlocked "外部"视图中是原子的.相同的内存位置.在32位模式下,原子保证仅适用于使用Interlocked(和可能是Volatile.*或者Thread.Volatile*,TBD?)函数的QWORD访问.

换句话说,要在32位模式下获得64位原子操作,所有对QWORD位置的访问必须通过Interlocked以保留保证,并且假设(例如)直接读取受到保护只是因为你不能变得可爱你总是使用Interlocked写作功能.

最后,需要注意的是,Interlocked在功能CLR专门认可,并接受特殊治疗,在.NET JIT编译器.看到这里这里这个事实可能有助于解释我之前提到的反直觉.


Gle*_*den 5

[编辑:] Mea culpa并向@AnorZaken 道歉,因为我的回答与他的相似。老实说,在发布我的之前我没有看到它。我会暂时保留这个,以防我的文字和解释有用或有其他见解,但之前工作的功劳要归功于 Anor。


尽管我在此页面上有另一个解决方案,但有些人可能对完全不同的方法感兴趣。下面,我给DynamicMethod它实现了Interlocked.CompareExchange任何32位或64位blittable类型,其中包括任何自定义Enum类型,原始类型的内置方法忘了(uintulong),甚至你自己的ValueType情况-只要任何的这些是双字4个字节,即intSystem.Int32),或qword的8个字节longSystem.Int64)的尺寸。例如,以下Enum类型不起作用,因为它指定了非默认大小byte

enum ByteSizedEnum : byte { Foo }     // no: size is not 4 or 8 bytes
Run Code Online (Sandbox Code Playgroud)

与运行时生成的IL 的大多数DynamicMethod实现一样,C#代码看起来并不漂亮,但对于某些人来说,优雅的 IL 和流畅的 JITted 本机代码弥补了这一点。例如,与我发布的另一种方法相比,这个方法不使用C# 代码。unsafe

为了允许在调用站点自动推断泛型类型,我将帮助器包装在一个static类中:

public static class IL<T> where T : struct
{
    // generic 'U' enables alternate casting for 'Interlocked' methods below
    public delegate U _cmp_xchg<U>(ref U loc, U _new, U _old);

    // we're mostly interested in the 'T' cast of it
    public static readonly _cmp_xchg<T> CmpXchg;

    static IL()
    {
        // size to be atomically swapped; must be 4 or 8.
        int c = Marshal.SizeOf(typeof(T).IsEnum ?
                                Enum.GetUnderlyingType(typeof(T)) :
                                typeof(T));

        if (c != 4 && c != 8)
            throw new InvalidOperationException("Must be 32 or 64 bits");

        var dm = new DynamicMethod(
            "__IL_CmpXchg<" + typeof(T).FullName + ">",
            typeof(T),
            new[] { typeof(T).MakeByRefType(), typeof(T), typeof(T) },
            MethodInfo.GetCurrentMethod().Module,
            false);

        var il = dm.GetILGenerator();
        il.Emit(OpCodes.Ldarg_0);    // ref T loc
        il.Emit(OpCodes.Ldarg_1);    // T _new
        il.Emit(OpCodes.Ldarg_2);    // T _old
        il.Emit(OpCodes.Call, c == 4 ?
                ((_cmp_xchg<int>)Interlocked.CompareExchange).Method :
                ((_cmp_xchg<long>)Interlocked.CompareExchange).Method);
        il.Emit(OpCodes.Ret);

        CmpXchg = (_cmp_xchg<T>)dm.CreateDelegate(typeof(_cmp_xchg<T>));
    }
};
Run Code Online (Sandbox Code Playgroud)

从技术上讲,以上就是您所需要的。现在,您可以拨打CmpXchgIL<T>.CmpXchg(...)任何适当的值类型(如上面的介绍中讨论),它将完全一样内置Interlocked.CompareExchange(...)System.Threading。例如,假设您有一个struct包含两个整数:

struct XY
{
    public XY(int x, int y) => (this.x, this.y) = (x, y);   // C#7 tuple syntax
    int x, y;
    static bool eq(XY a, XY b) => a.x == b.x && a.y == b.y;
    public static bool operator ==(XY a, XY b) => eq(a, b);
    public static bool operator !=(XY a, XY b) => !eq(a, b);
}
Run Code Online (Sandbox Code Playgroud)

您现在可以像您期望的任何CmpXchg操作一样以原子方式发布 64 位结构。这以原子方式发布了两个整数,因此另一个线程不可能看到“撕裂”或不一致的配对。不用说,使用逻辑配对轻松实现这一点在并发编程中非常有用,如果您设计了一个精心设计的结构,将许多字段打包到可用的 64(或 32)位中,则更是如此。这是执行此操作的调用站点示例:

var xy = new XY(3, 4);      // initial value

//...

var _new = new XY(7, 8);    // value to set
var _exp = new XY(3, 4);    // expected value

if (IL<XY>.CmpXchg(ref xy, _new, _exp) != _exp)  // atomically swap the 64-bit ValueType
    throw new Exception("change not accepted");
Run Code Online (Sandbox Code Playgroud)

上面,我提到您可以通过启用类型推断来整理调用站点,这样您就不必指定泛型参数。为此,只需在您的通用全局类之一中定义一个静态通用方法

public static class my_globals
{
    [DebuggerStepThrough, MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static T CmpXchg<T>(ref T loc, T _new, T _old) where T : struct => 
                                                 _IL<T>.CmpXchg(ref loc, _new, _old);
}
Run Code Online (Sandbox Code Playgroud)

我将使用不同的示例展示简化的调用站点,这次使用的是Enum

using static my_globals;

public enum TestEnum { A, B, C };

static void CompareExchangeEnum()
{
    var e = TestEnum.A;

    if (CmpXchg(ref e, TestEnum.B, TestEnum.A) != TestEnum.A)
        throw new Exception("change not accepted");
}
Run Code Online (Sandbox Code Playgroud)

至于原来的问题,ulong以及uint工作平凡,以及:

ulong ul = 888UL;

if (CmpXchg(ref ul, 999UL, 888UL) != 888UL)
    throw new Exception("change not accepted");
Run Code Online (Sandbox Code Playgroud)