C # Interlocked Exchange - PullRequest
       12

C # Interlocked Exchange

6 голосов
/ 13 июля 2009

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

public static float Time;

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

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

Ответы [ 6 ]

17 голосов
/ 13 июля 2009

Существует перегрузка для 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 не реализовала его для всех примитивных типов. Я не могу себе представить, что это было бы трудно сделать (в конце концов, это всего лишь биты), но, вероятно, они хотели уменьшить количество перегрузок.

14 голосов
/ 08 апреля 2011

Хотя это некрасиво, но - это , на самом деле можно выполнить атомарное Exchange или CompareExchange для перечисления или другого типа значения blittable в 64 бит или менее, используя 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 года] Теперь вы можете сделать это более эффективно с помощью 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-битном режиме (или, то есть в подсистеме WOW ), 64-битная Операции Interlocked не гарантированно являются атомарными по отношению к non- Interlocked, «внешним» представлениям тех же областей памяти. В 32-битном режиме атомарная гарантия применяется только глобально к доступам QWORD, которые используют функции Interlocked (и, возможно, Volatile.*, или Thread.Volatile*, TBD?).

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

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

3 голосов
/ 09 февраля 2018

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


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

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

Как и в большинстве DynamicMethod реализаций сгенерированного во время выполнения IL , код C # не очень красив, но для некоторых людей элегантный IL изящный нативный код JITted восполняет это. Например, в отличие от другого метода, который я выложил, этот не использует unsafe C # код.

Чтобы разрешить автоматический вывод универсального типа на сайте вызова, я заключаю помощника в класс 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) бита. Вот пример call-сайта для этого:

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");
3 голосов
/ 04 июня 2015

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

3 голосов
/ 13 июля 2009

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

0 голосов
/ 13 июля 2009

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

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