Хотя это некрасиво, но - это , на самом деле можно выполнить атомарное 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. См. здесь и здесь Этот факт может помочь объяснить нелогичность, о которой я упоминал ранее.