Вопрос

У меня есть часть моей игры, которая выглядит так:

public static float Time;

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

Я хочу изменить Time на Uint32;однако, когда я пытаюсь использовать UInt32 вместо float для значений он заявляет, что тип должен быть ссылочным типом. Float не является ссылочным типом, поэтому я знаю, что технически возможно сделать это с нессылочными типами.Есть ли какой-нибудь практический способ заставить эту работу работать с UInt32?

Это было полезно?

Решение

Хоть и некрасиво, но является на самом деле возможно выполнить атомарное Обмен или СравнитьОбменять для перечисления или другого преобразуемого типа значения длиной 64 бита или меньше, используя 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
        }
}

Противоречивая часть состоит в том, что ссылка выражение для разыменованного указателя делает фактически проникнуть по адресу перечисления.Я думаю, что компилятор имел бы право вместо этого сгенерировать невидимую временную переменную в стеке, и в этом случае это не сработало бы.Используйте на свой риск.

[редактировать:для конкретного типа, запрошенного ФП]

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 г.] Теперь вы можете сделать это более эффективно, используя 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-битных примитивных типов (byte, sbyte, char, ushort, short) без риска побочного повреждения соседних байтов.В следующем примере BadEnum явно указывает размер, который слишком мал для атомарной замены без возможного воздействия до трех соседних байтов.

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

Если вы не ограничены макетами, определяемыми взаимодействием (или иным образом фиксированными), обходным решением будет обеспечение того, чтобы макет памяти таких перечислений всегда дополнялся до минимума в 4 байта, чтобы обеспечить атомарную замену (как int).Однако вполне вероятно, что это приведет к сведению на нет цели, с которой изначально было указано меньшую ширину.



[редактировать:апрель 2017 г.] Недавно я узнал, что когда .NET работает в 32-битном режиме (или, т.е.в УХ ТЫ подсистема), 64-битная Interlocked операции нет гарантированно будет атомарным по отношению к не-Interlocked, «внешние» представления тех же ячеек памяти.В 32-битном режиме атомарная гарантия применяется только глобально для всех обращений QWORD, которые используют Interlocked (и, возможно Volatile.*, или Thread.Volatile*, TBD?) функции.

Другими словами, чтобы получить 64-битные атомарные операции в 32-битном режиме, все доступ к локациям QWORD должен осуществляться через Interlocked чтобы сохранить гарантии, и вы не можете лукавить, предполагая, что (например) прямое чтение защищено только потому, что вы всегда используете Interlocked функции для записи.

Наконец, обратите внимание, что Interlocked функции в CLR специально распознаются и обрабатываются JIT-компилятором .NET.Видеть здесь и здесь Этот факт может помочь объяснить нелогичность, о которой я упоминал ранее.

Другие советы

Есть перегрузка для Interlocked.Exchange специально для float (и другие для double, int, long, IntPtr и object).Для uint его нет, поэтому компилятор считает, что наиболее близким совпадением является общий Interlocked.Exchange<T> - но в таком случае T должен быть ссылочным типом. uint не является ссылочным типом, поэтому это тоже не работает - отсюда и сообщение об ошибке.

Другими словами:

  • Ваш текущий код работает, потому что он вызывает Interlocked.Exchange(ref float, float).
  • Изменение его на uint терпит неудачу, поскольку нет применимой перегрузки.Точное сообщение об ошибке вызвано тем, что компилятор догадается, что вы имеете в виду Interlocked.Exchange<T>(ref T, T).

Что касается того, что делать, то варианты любые:

  • Потенциально использовать int вместо этого, как предлагает Марк.
  • Если вам нужен дополнительный диапазон, подумайте об использовании long.
  • Использовать uint но не пытайтесь писать код без блокировки

Хотя очевидно Exchange отлично работает с некоторый конкретных типов значений, Microsoft не реализовала это для всех примитивных типов.Я не могу себе представить, что это было бы сложно сделать (в конце концов, это всего лишь биты), но, по-видимому, они хотели сократить отсчет перегрузки.

Возможно, используйте int вместо uint;есть перегрузки для int.Вам нужен дополнительный немного дальности?Если да, то кастуйте/конвертируйте как можно позже.

Это все еще хак, но это можно сделать с помощью 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 для Enums, который можно найти здесь.

Генерация метода при первом вызове потребует некоторых накладных расходов, но сгенерированный метод сохраняется в форме делегата, поэтому любые последующие вызовы должны быть очень производительными.

И цитирую по ссылке выше:

Сгенерированный IL можно проверить, по крайней мере, согласно PEVerify, что можно проверить, воспользовавшись этим использованием. AssemblyBuilder и сохранение результата в файл.

[редактировать:] Моя вина и извиняюсь перед @AnorZaken, так как мой ответ похож на его.Я, честно говоря, не видел этого до того, как опубликовал свой.Я пока сохраню это на случай, если мой текст и объяснения окажутся полезными или принесут дополнительную информацию, но заслуга за предыдущую работу принадлежит Анору.


Хотя у меня есть другое решение на этой странице некоторых людей может заинтересовать совершенно другой подход.Ниже я даю DynamicMethod который реализует Interlocked.CompareExchange для любой 32- или 64-битный преобразуемый тип, который включает в себя любые пользовательские Enum типы, примитивные типы, которые забыл встроенный метод (uint, ulong), и даже свой собственный ValueType случаи - при условии, что любой из них двойное слово (4 байта, то есть, int, System.Int32) или qword (8 байт, long, System.Int64) по размеру.Например, следующее Enum тип не будет работает, поскольку он указывает размер, отличный от значения по умолчанию, byte:

enum ByteSizedEnum : byte { Foo }     // no: size is not 4 or 8 bytes

Как и большинство ДинамическийМетод реализации генерируемых во время выполнения Иллинойс, С# Код некрасив на вид, но для некоторых людей элегантный IL и гладкий JIT-код компенсируют это.Например, в отличие от другого метода, который я опубликовал, этот не использует 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 содержащий два целых числа:

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

Ты можешь сейчас атомарно опубликовать 64-битную структуру как и следовало ожидать от любого CmpXchg операция.При этом два целых числа публикуются атомарно, так что другой поток не может увидеть «разорванное» или противоречивое соединение.Излишне говорить, что легко сделать это с помощью логического соединения чрезвычайно полезно в параллельном программировании, особенно если вы разрабатываете сложную структуру, которая упаковывает множество полей в доступные 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");

Вы не можете передать приведенное выражение по ссылке, вам следует использовать временную переменную:

public static float Time;
float value2 = (float)SomeValue;
Interlocked.Exchange(ref Time, ref value2);
SomeValue = value2;
Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top