我的游戏有点像这样:
public static float Time;
float someValue = 123;
Interlocked.Exchange(ref Time, someValue);
我想将 Time 更改为 Uint32;但是,当我尝试使用UInt32
而不是float
值时,它会抗议该类型必须是引用类型。Float
不是引用类型,所以我知道在技术上可以使用非引用类型来做到这一点。有什么实用的方法可以使它起作用UInt32
吗?
我的游戏有点像这样:
public static float Time;
float someValue = 123;
Interlocked.Exchange(ref Time, someValue);
我想将 Time 更改为 Uint32;但是,当我尝试使用UInt32
而不是float
值时,它会抗议该类型必须是引用类型。Float
不是引用类型,所以我知道在技术上可以使用非引用类型来做到这一点。有什么实用的方法可以使它起作用UInt32
吗?
有一个重载Interlocked.Exchange
专门用于(以及float
其他用于double
、int
、long
和)。没有一个用于 uint,因此编译器认为最接近的匹配是泛型- 但在这种情况下必须是引用类型。不是引用类型,因此也不起作用 - 因此出现错误消息。IntPtr
object
Interlocked.Exchange<T>
T
uint
换句话说:
Interlocked.Exchange(ref float, float)
.uint
失败,因为没有适用的过载。确切的错误消息是由编译器猜测您的意思引起的Interlocked.Exchange<T>(ref T, T)
。至于做什么,选项是:
int
正如 Marc 建议的那样,可能会使用它。long
.uint
但不要尝试编写无锁代码尽管对于某些Exchange
特定的值类型显然可以正常工作,但 Microsoft 并没有为所有原始类型实现它。我无法想象这样做会很困难(毕竟它们只是一点点),但大概他们想保持超载倒计时。
虽然丑陋,但实际上可以使用C# 代码对 64 位或更少的枚举或其他 blittable 值类型执行原子交换或比较交换:unsafe
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
来实现这一点,但这不起作用,因为您不能ref
在 dreferenced 上使用__refvalue
。这太糟糕了,因为 CLR 将函数映射到在[comment mooted InterlockedExchange
TypedReference
] 上运行的私有内部函数通过 JIT 拦截,见下文])
[编辑:2018 年 7 月]您现在可以使用System.Runtime.CompilerServices.Unsafe库包更有效地执行此操作。您的方法可以用来直接重新解释目标托管引用所引用的类型,避免固定和转换到模式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);
当然,这也适用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);
这也适用于枚举类型——但前提是它们的底层原始整数正好是 4 或 8 个字节。换句话说,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);
我不确定 4 字节最小值是否是 .NET 的基础,但据我所知,它无法原子交换(值)较小的 8 位或 16 位原始类型(byte
, sbyte
, char
, ushort
, short
) 而不会冒对相邻字节造成附带损害的风险。在下面的示例中,BadEnum
显式指定了一个太小而无法以原子方式交换而不会影响最多三个相邻字节的大小。
enum BadEnum : byte { }; // can't swap less than 4 bytes on .NET?
如果您不受互操作指定(或以其他方式固定)布局的限制,解决方法是确保此类枚举的内存布局始终填充到最小 4 字节以允许原子交换(如int
)。然而,这样做似乎很可能会破坏最初指定较小宽度的任何目的。
[编辑:2017 年 4 月]我最近了解到,当.NET
以 32 位模式(或者,即在WOW子系统中)运行时,64 位Interlocked
操作不能保证相对于非Interlocked
“外部”视图是原子的相同的内存位置。Interlocked
在 32 位模式下,原子保证仅适用于使用(可能还有Volatile.*
,或Thread.Volatile*
,待定?)函数的 QWORD 访问。
换句话说,要在 32 位模式下获得 64 位原子操作,对这些 QWORD 位置的所有访问都必须通过Interlocked
/Volatile
进行,以保持保证,因此假设(例如)直接(即非Interlocked
/ Volatile
)读取受到保护只是因为您总是使用Interlocked
/Volatile
函数进行写入。
最后,请注意.NET JIT 编译器特别识别并接受特殊处理中的Interlocked
函数。CLR
见这里和这里这个事实可能有助于解释我之前提到的反直觉。
[编辑:] Mea culpa并向@AnorZaken 道歉,因为我的回答与他的相似。老实说,在发布我的之前我没有看到它。我暂时保留这个,以防我的文字和解释有用或有其他见解,但先前工作的功劳归于 Anor。
尽管我在此页面上有另一个解决方案,但有些人可能对完全不同的方法感兴趣。下面,我给出了任何32 位或 64 位 blittable 类型的DynamicMethod
实现,它包括任何自定义类型、内置方法忘记的原始类型 ( , ),甚至是您自己的实例——只要任何这些是dword ( 4-bytes , 即 , )或qword ( 8-bytes , , ) 大小的。例如,以下类型将不起作用,因为它指定了非默认大小:Interlocked.CompareExchange
Enum
uint
ulong
ValueType
int
System.Int32
long
System.Int64
Enum
byte
enum ByteSizedEnum : byte { Foo } // no: size is not 4 or 8 bytes
与大多数运行时生成的IL的DynamicMethod实现一样,C#代码看起来并不漂亮,但对于某些人来说,优雅的 IL 和流畅的 JIT 原生代码弥补了这一点。例如,与我发布的另一种方法相比,这个方法不使用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>));
}
};
从技术上讲,以上就是您所需要的。您现在可以调用CmpXchgIL<T>.CmpXchg(...)
任何适当的值类型(如上面的介绍中所述),它的行为与内置Interlocked.CompareExchange(...)
的System.Threading
. 例如,假设您有一个struct
包含两个整数的 a:
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);
}
您现在可以像对任何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");
上面,我提到您可以通过启用类型推断来整理调用站点,这样您就不必指定泛型参数。为此,只需在您的一个非泛型全局类中定义一个静态泛型方法:
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);
}
我将用一个不同的例子展示简化的呼叫站点,这次使用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");
}
至于最初的问题,ulong
并且uint
工作也很简单:
ulong ul = 888UL;
if (CmpXchg(ref ul, 999UL, 888UL) != 888UL)
throw new Exception("change not accepted");
也许使用int
代替uint
; 有重载int
。你需要额外的范围吗?如果是这样,请尽可能晚地进行转换/转换。
它仍然是一个 hack,但可以通过 IL 生成而不是使用unsafe
代码来做到这一点。好处是,它不依赖于编译器实现细节,而是依赖于有符号和无符号类型具有相同位长的事实,这是规范的一部分。
方法如下:
using System;
using System.Reflection;
using System.Reflection.Emit;
using ST = System.Threading;
/// <summary>
/// Provides interlocked methods for uint and ulong via IL-generation.
/// </summary>
public static class InterlockedUs
{
/// <summary>
/// Compares two 32-bit unsigned integers for equality and, if they are equal,
/// replaces one of the values.
/// </summary>
/// <param name="location">
/// The value to exchange, i.e. the value that is compared with <paramref name="comparand"/> and
/// possibly replaced with <paramref name="value"/>.</param>
/// <param name="value">
/// The value that replaces the <paramref name="location"/> value if the comparison
/// results in equality.</param>
/// <param name="comparand">
/// A value to compare against the value at <paramref name="location"/>.</param>
/// <returns>The original value in <paramref name="location"/>.</returns>
public static uint CompareExchange(ref uint location, uint value, uint comparand)
{
return ceDelegate32(ref location, value, comparand);
}
/// <summary>
/// Compares two 64-bit unsigned integers for equality and, if they are equal,
/// replaces one of the values.
/// </summary>
/// <param name="location">
/// The value to exchange, i.e. the value that is compared with <paramref name="comparand"/> and
/// possibly replaced with <paramref name="value"/>.</param>
/// <param name="value">
/// The value that replaces the <paramref name="location"/> value if the comparison
/// results in equality.</param>
/// <param name="comparand">
/// A value to compare against the value at <paramref name="location"/>.</param>
/// <returns>The original value in <paramref name="location"/>.</returns>
public static ulong CompareExchange(ref ulong location, ulong value, ulong comparand)
{
return ceDelegate64(ref location, value, comparand);
}
#region --- private ---
/// <summary>
/// The CompareExchange signature for uint.
/// </summary>
private delegate uint Delegate32(ref uint location, uint value, uint comparand);
/// <summary>
/// The CompareExchange signature for ulong.
/// </summary>
private delegate ulong Delegate64(ref ulong location, ulong value, ulong comparand);
/// <summary>
/// IL-generated CompareExchange method for uint.
/// </summary>
private static readonly Delegate32 ceDelegate32 = GenerateCEMethod32();
/// <summary>
/// IL-generated CompareExchange method for ulong.
/// </summary>
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
}
归功于“hvd”的 IL 生成想法和枚举的 CompareExchange 方法的类似代码,可以在此处找到。
在第一次调用时生成方法会有一些开销,但生成的方法以委托形式存储,因此任何后续调用都应该非常高效。
并引用上面的链接:
至少根据 PEVerify,生成的 IL 是可验证的,可以通过使用此功能
AssemblyBuilder
并将结果保存到文件来进行检查。
您不能通过引用传递转换表达式,您应该使用临时变量:
public static float Time;
float value2 = (float)SomeValue;
Interlocked.Exchange(ref Time, ref value2);
SomeValue = value2;