我的游戏有点像这样:

public static float Time;

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

我想将时间更改为uint32;但是,当我尝试对值使用UInt32而不是float时,它会抗议类型必须是引用类型。Float不是引用类型,所以我知道在技术上可以对非引用类型执行此操作。有没有什么实用的方法可以让它与UInt32一起工作?

最佳答案

虽然难看,但实际上可以使用unsafec代码对枚举或其他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位原语类型(bytesbytecharushortshortBadEnumint)的值,而不会对相邻字节造成附带损害。在下面的示例中,.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/

10-09 19:22