我的游戏有点像这样:
public static float Time;
float someValue = 123;
Interlocked.Exchange(ref Time, someValue);
我想将时间更改为uint32;但是,当我尝试对值使用
UInt32
而不是float
时,它会抗议类型必须是引用类型。Float
不是引用类型,所以我知道在技术上可以对非引用类型执行此操作。有没有什么实用的方法可以让它与UInt32
一起工作? 最佳答案
虽然难看,但实际上可以使用unsafe
c代码对枚举或其他64位或更少的blittable值类型执行原子交换或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
来实现这一点,但这不起作用,因为不能对引用的ref
使用__refvalue
。这太糟糕了,因为clr将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
,int
)的值,而不会对相邻字节造成附带损害。在下面的示例中,.NET
显式指定的大小太小,无法在不影响最多三个相邻字节的情况下进行原子交换。enum BadEnum : byte { }; // can't swap less than 4 bytes on .NET?
如果不受互操作指示(或其他固定)布局的约束,则解决方法是确保此类枚举的内存布局始终填充到4字节的最小值,以允许原子交换(如
Interlocked
)。然而,这样做可能会挫败最初指定较小宽度的任何目的。[编辑:2017年4月]我最近了解到,当
Interlocked
以32位模式(或在WOW子系统中)运行时,对于相同内存位置的非Interlocked
,“外部”视图,64位Volatile.*
操作不能保证是原子的。在32位模式下,原子保证只适用于使用Thread.Volatile*
(可能还有Interlocked
,或Interlocked
,tbd?)的qword访问。功能。换言之,要在32位模式下获得64位原子操作,所有对qword位置的访问都必须通过
Interlocked
来保持保证,并且假设(例如)直接读取是受保护的(因为您总是使用CLR
函数进行写入),则无法获得可爱的效果。最后,请注意,中的函数由.net jit编译器特别识别,并在中接受特殊处理。这个事实可能有助于解释我前面提到的反直觉。
关于c# - C#互锁交换,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/1116790/