C#Interlocked Exchange

我的游戏有点像这样:

public static float Time; float someValue = 123; Interlocked.Exchange(ref Time, someValue); 

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

虽然很难看,但实际上可以使用unsafe C#代码在枚举或其他值为64位或更低的blittable值类型上执行primefaces交换CompareExchange

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

违反直觉的部分是解除引用指针上的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); } 

[编辑:和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); } 

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


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

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

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

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

这也适用于枚举类型 – 但只要它们的基础原始整数恰好是四个或八个字节。 换句话说, 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(ref target), (int)value, (int)expected); static MyEnum Exchange(ref MyEnum target, MyEnum value) => (MyEnum)Interlocked.Exchange(ref Unsafe.As(ref target), (int)value); 

我不确定4字节的最小值是否是.NET的基础,但据我所知,它没有留下primefaces交换(值)较小的8位或16位基元类型( bytesbytecharushortshort )没有冒险对相邻字节造成附带损害的风险。 在下面的示例中, BadEnum显式指定的大小太小而无法自动交换而不会影响最多三个相邻字节。

 enum BadEnum : byte { }; // can't swap less than 4 bytes on .NET? 

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


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

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

最后,请注意CLR中的Interlocked函数由.NET JIT编译器特别识别并接受特殊处理。 看到这里和这里这个事实可能有助于解释我之前提到的反直觉。

Interlocked.Exchange特别适用于float (以及其他用于doubleintlongIntPtrobject )的重载。 没有一个用于uint,因此编译器认为最接近的匹配是通用的Interlocked.Exchange – 但在这种情况下T必须是引用类型。 uint不是引用类型,因此也不起作用 – 因此出现错误消息。

换一种说法:

  • 您当前的代码有效,因为它调用Interlocked.Exchange(ref float, float)
  • 将其更改为uint失败,因为没有适用的过载。 确切的错误消息是由编译器猜测你的意思是Interlocked.Exchange(ref T, T)

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

  • 正如Marc建议的那样,可能会使用int
  • 如果您需要额外的范围,请考虑使用long
  • 使用uint但不要尝试编写无锁代码

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

也许使用int而不是uint ; int有重载。 你需要额外的范围吗? 如果是这样,尽可能晚地施放/转换。

它仍然是一个黑客,但它可以用IL生成而不是使用unsafe代码。 好处是,它不依赖于编译器实现细节,而是依赖于有符号和无符号类型具有相同位长的事实,这是规范的一部分。

方法如下:

 using System; using System.Reflection; using System.Reflection.Emit; using ST = System.Threading; ///  /// Provides interlocked methods for uint and ulong via IL-generation. ///  public static class InterlockedUs { ///  /// Compares two 32-bit unsigned integers for equality and, if they are equal, /// replaces one of the values. ///  ///  /// The value to exchange, ie the value that is compared with  and /// possibly replaced with . ///  /// The value that replaces the  value if the comparison /// results in equality. ///  /// A value to compare against the value at . /// The original value in . public static uint CompareExchange(ref uint location, uint value, uint comparand) { return ceDelegate32(ref location, value, comparand); } ///  /// Compares two 64-bit unsigned integers for equality and, if they are equal, /// replaces one of the values. ///  ///  /// The value to exchange, ie the value that is compared with  and /// possibly replaced with . ///  /// The value that replaces the  value if the comparison /// results in equality. ///  /// A value to compare against the value at . /// The original value in . public static ulong CompareExchange(ref ulong location, ulong value, ulong comparand) { return ceDelegate64(ref location, value, comparand); } #region --- private --- ///  /// The CompareExchange signature for uint. ///  private delegate uint Delegate32(ref uint location, uint value, uint comparand); ///  /// The CompareExchange signature for ulong. ///  private delegate ulong Delegate64(ref ulong location, ulong value, ulong comparand); ///  /// IL-generated CompareExchange method for uint. ///  private static readonly Delegate32 ceDelegate32 = GenerateCEMethod32(); ///  /// IL-generated CompareExchange method for ulong. ///  private static readonly Delegate64 ceDelegate64 = GenerateCEMethod64(); private static Delegate32 GenerateCEMethod32() { const string name = "CompareExchange"; Type signedType = typeof(int), unsignedType = typeof(uint); var dm = new DynamicMethod(name, unsignedType, new[] { unsignedType.MakeByRefType(), unsignedType, unsignedType }); var ilGen = dm.GetILGenerator(); ilGen.Emit(OpCodes.Ldarg_0); ilGen.Emit(OpCodes.Ldarg_1); ilGen.Emit(OpCodes.Ldarg_2); ilGen.Emit( OpCodes.Call, typeof(ST.Interlocked).GetMethod(name, BindingFlags.Public | BindingFlags.Static, null, new[] { signedType.MakeByRefType(), signedType, signedType }, null)); ilGen.Emit(OpCodes.Ret); return (Delegate32)dm.CreateDelegate(typeof(Delegate32)); } private static Delegate64 GenerateCEMethod64() { const string name = "CompareExchange"; Type signedType = typeof(long), unsignedType = typeof(ulong); var dm = new DynamicMethod(name, unsignedType, new[] { unsignedType.MakeByRefType(), unsignedType, unsignedType }); var ilGen = dm.GetILGenerator(); ilGen.Emit(OpCodes.Ldarg_0); ilGen.Emit(OpCodes.Ldarg_1); ilGen.Emit(OpCodes.Ldarg_2); ilGen.Emit( OpCodes.Call, typeof(ST.Interlocked).GetMethod(name, BindingFlags.Public | BindingFlags.Static, null, new[] { signedType.MakeByRefType(), signedType, signedType }, null)); ilGen.Emit(OpCodes.Ret); return (Delegate64)dm.CreateDelegate(typeof(Delegate64)); } #endregion } 

感谢IL-generation想法的“hvd”和Enums的CompareExchange方法的类似代码,可在此处找到。

在第一次调用时生成方法会有一些开销,但生成的方法以委托forms存储,因此任何后续调用都应该非常高效。

并引用上面的链接:

生成的IL是可validation的,至少根据PEVerify,可以通过使用AssemblyBuilder并将结果保存到文件来检查。

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


虽然我在这个页面上有另一种解决方案 ,但有些人可能会对完全不同的方法感兴趣。 下面,我给出一个DynamicMethod ,它为任何32位或64位blittable类型实现Interlocked.CompareExchange ,其中包括任何自定义Enum类型,内置方法忘记的基本类型( uintulong ),甚至是你自己的ValueType实例 – 只要其中任何一个是dword4字节 ,即intSystem.Int32 )或qword8字节longSystem.Int64 )大小。 例如,以下Enum类型将无法工作,因为它指定了非默认大小byte

 enum ByteSizedEnum : byte { Foo } // no: size is not 4 or 8 bytes 

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

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

 public static class IL where T : struct { // generic 'U' enables alternate casting for 'Interlocked' methods below public delegate U _cmp_xchg(ref U loc, U _new, U _old); // we're mostly interested in the 'T' cast of it public static readonly _cmp_xchg 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)Interlocked.CompareExchange).Method : ((_cmp_xchg)Interlocked.CompareExchange).Method); il.Emit(OpCodes.Ret); CmpXchg = (_cmp_xchg)dm.CreateDelegate(typeof(_cmp_xchg)); } }; 

从技术上讲,以上就是您所需要的。 您现在可以在任何适当的值类型上调用CmpXchgIL.CmpXchg(...) (如上面的介绍中所述),它的行为与System.Threading内置的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) => ax == bx && ay == by; public static bool operator ==(XY a, XY b) => eq(a, b); public static bool operator !=(XY a, XY b) => !eq(a, b); } 

您现在可以像在任何CmpXchg操作中一样, 以primefaces方式发布64位结构 。 这以primefaces方式发布两个整数,这样另一个线程就不可能看到“撕裂”或不一致的配对。 毋庸置疑,使用逻辑配对轻松实现这一点在并发编程中非常有用,如果您设计一个精心设计的结构,将许多字段打包到可用的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.CmpXchg(ref xy, _new, _exp) != _exp) // atomically swap the 64-bit ValueType throw new Exception("change not accepted"); 

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

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

我将使用不同的示例显示简化的呼叫站点,这次使用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"); } 

至于原来的问题, ulonguint工作也很简单:

 ulong ul = 888UL; if (CmpXchg(ref ul, 999UL, 888UL) != 888UL) throw new Exception("change not accepted"); 

您不能通过引用传递已转换的表达式,您应该使用临时变量:

 public static float Time; float value2 = (float)SomeValue; Interlocked.Exchange(ref Time, ref value2); SomeValue = value2;