C#联锁交换

时间:2009-07-12 20:06:58

标签: c# interlocked

我的游戏有点像这样:

public static float Time;

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

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

6 个答案:

答案 0 :(得分:17)

Interlocked.Exchange专门针对float(以及其他doubleintlongIntPtr和{{1}的重载}})。没有一个用于uint,因此编译器认为最接近的匹配是泛型object - 但在这种情况下Interlocked.Exchange<T>必须是引用类型。 T不是引用类型,因此也不起作用 - 因此错误消息。

换句话说:

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

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

虽然uint显然适用于某些特定值类型,但Microsoft尚未针对所有基本类型实现它。我无法想象这样做会很难(毕竟它们只是比特),但可能他们想要保持过载数量下降。

答案 1 :(得分:14)

虽然很难看, 实际上可以在枚举或其他blittable值类型64位上执行原子 Exchange CompareExchange 或少用unsafe C#代码:

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

这也适用于枚举类型 - 但只要它们的基础原始整数恰好是四个或八个字节。换句话说,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字节以允许进行原子交换(如{ {1}})。然而,似乎这样做可能会破坏首先指定较小​​宽度的任何目的。



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

换句话说,要在32位模式下获取64位原子操作,必须通过Thread.Volatile*对强制位置进行 所有 访问,以便保留保证,假设(例如)直接读取受到保护只是因为你总是使用Interlocked函数进行写作,你就不会变得可爱。

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

答案 2 :(得分:3)

也许使用int代替uint; int有重载。你需要额外的范围吗?如果是这样,尽可能晚地进行演员/转换。

答案 3 :(得分:3)

它仍然是一个黑客,但可以使用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生成理念以及针对Enums的CompareExchange方法的类似代码,可以找到here

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

引用上述链接:

  

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

答案 4 :(得分:3)

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

虽然我在此页面上有another solution,但有些人可能对完全不同的方法感兴趣。下面,我为{strong> 任意32位或64位blittable类型 提供DynamicMethod Interlocked.CompareExchange,其中包含任何自定义{{1} } types,内置方法忘记的基本类型(Enumuint),甚至是你自己的ulong实例 - 只要其中任何一个都是 dword 4字节,即ValueTypeint)或 qword 8字节System.Int32long)大小。例如,以下System.Int64类型赢了&#em> ,因为它指定了非默认大小Enum

byte

与运行时生成的 IL 的大多数DynamicMethod实现一样, C#代码并不美观,但对于某些人来说,优雅IL和时尚的JITted本机代码弥补了这一点。例如,与我发布的其他方法相比,这个方法不使用enum ByteSizedEnum : byte { Foo } // no: size is not 4 or 8 bytes 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

您现在可以以原子方式发布64位结构,就像您对任何 CmpXchg 操作一样。这自动地发布了两个整数,这样另一个线程就不可能看到一个“撕裂”的整数。或不一致的配对。毋庸置疑,使用逻辑配对轻松实现这一点在并发编程中非常有用,如果您设计一个精心设计的结构,将许多字段打包到可用的64位(或32位)中,则更是如此。以下是调用网站的示例:

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

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

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

答案 5 :(得分:-3)

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

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