C#互锁交换 [英] C# Interlocked Exchange

查看:94
本文介绍了C#互锁交换的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我的游戏看起来像这样:

public static float Time;

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

我想将Time更改为Uint32;但是,当我尝试使用UInt32而不是float作为值时,它抗议该类型必须是引用类型. Float不是引用类型,所以我知道使用非引用类型来做到这一点在技术上是可行的.有什么实用的方法可以使UInt32起作用吗?

解决方案

虽然很丑,但实际上有可能执行原子的 Exchange CompareExchange 使用unsafe C#代码对64位或更少的枚举或其他可转换值类型进行"strong>强":

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 表达式确实会穿透到枚举的地址.我认为编译器将有权在堆栈上生成一个不可见的临时变量,在这种情况下,它将不起作用.使用风险自负.

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

[edit:和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来实现此目的,但这不起作用,因为您不能在引用的__refvalue上使用ref.这太糟糕了,因为CLR将InterlockedExchange函数映射到在TypedReference 上运行的私有内部函数[由JIT拦截提出的评论,请参阅下文])


,您现在可以使用 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);

当然这也适用于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);

这也适用于枚举类型-但前提是它们的基础原始整数恰好是四个或八个字节.换句话说,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位原始类型(的值)的方法( bytesbytecharushortshort),而不会对相邻字节造成附带损害.在下面的示例中,BadEnum显式指定的大小太小而无法原子交换,而不会影响最多三个相邻字节.

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

如果不受互斥(或固定)布局的约束,一种变通方法是确保此类枚举的内存布局始终填充到最小4字节,以允许原子交换(如int).但是,这样做似乎有可能破坏最初指定较小宽度的目的.



我最近了解到,当.NET在32位模式下运行时(即,在Interlocked(可能是Volatile.*Thread.Volatile*,TBD?)功能的QWORD访问.

换句话说,要获得32位模式下的64位原子操作,必须通过Interlocked进行对这些QWORD位置的 all 访问,以便保留保证,并且假设(例如)直接读取受到保护,仅仅是因为您始终使用Interlocked函数进行写操作,就不会显得可爱.

最后,请注意,.NET JIT编译器对CLR中的Interlocked函数进行了特殊识别,并对其进行了特殊处理.请参见此处 解决方案

Although ugly, it is actually possible to perform an atomic Exchange or CompareExchange on an enum or other blittable value type of 64 bits or less using unsafe C# code:

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

The counterintuitive part is that the ref expression on the dereferenced pointer does actually penetrate through to the address of the enum. I think the compiler would have been within its rights to have generated an invisible temporary variable on the stack instead, in which case this wouldn't work. Use at your own risk.

[edit: for the specific type requested by the 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);
}

[edit: and 64-bit unsigned long]

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

(I also tried using the undocumented C# keyword __makeref to achieve this, but this doesn't work because you can't use ref on a dreferenced __refvalue. It's too bad, because the CLR maps the InterlockedExchange functions to a private internal function that operates on TypedReference [comment mooted by JIT interception, see below])


[edit: July 2018] You can now do this more efficiently using the System.Runtime.CompilerServices.​Unsafe library package. Your method can use Unsafe.As<TFrom,TTo>() to directly reinterpret the type referenced by the target managed reference, avoiding the dual expenses of both pinning and transitioning to unsafe mode:

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);

Of course this works for Interlocked.Exchange as well. Here are those helpers for the 4- and 8-byte unsigned types.

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);

This works for enumeration types also--but only so long as their underlying primitive integer is exactly four or eight bytes. In other words, int (32-bit) or long (64-bit) sized. The limitation is that these are the only two bit-widths found among the Interlocked.CompareExchange overloads. By default, enum uses int when no underlying type is specified, so MyEnum (from above) works fine.

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);

I'm not sure whether the 4-byte minimum is a fundamental to .NET, but as far as I can tell it leaves no means of atomically swapping (values of) the smaller 8- or 16-bit primitive types (byte, sbyte, char, ushort, short) without risking collateral damage to adjacent byte(s). In the following example, BadEnum explicitly specifies a size that is too small to be atomically swapped without possibly affecting up to three neighboring bytes.

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

If you're not constrained by interop-dictated (or otherwise fixed) layouts, a workaround would be to ensure that the memory layout of such enums is always padded to the 4-byte minimum to allow for atomic swapping (as int). It seems likely, however, that doing so would defeat whatever purpose there might have been for specifying the smaller width in the first place.



[edit: April 2017] I recently learned that when .NET is running in 32-bit mode (or, i.e. in the WOW subsystem), the 64-bit Interlocked operations are not guaranteed to be atomic with respect to non-Interlocked, "external" views of the same memory locations. In 32-bit mode, the atomic guarantee only applies globablly across QWORD accesses which use the Interlocked (and perhaps Volatile.*, or Thread.Volatile*, TBD?) functions.

In other words, to obtain 64-bit atomic operations in 32-bit mode, all accesses to those QWORD locations must occur through Interlocked in order to preserve the guarantees, and you can't get cute assuming that (e.g.) direct reads are protected just because you always use Interlocked functions for writing.

Finally, note that the Interlocked functions in the CLR are specially recognized by, and receive special treatment in, the .NET JIT compiler. See here and here This fact may help explain the counter-intuitiveness I mentioned earlier.

这篇关于C#互锁交换的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆