Interlocked.Exchange
пытается использовать атомарные инструкции ЦП на любой платформе, на которой вы работаете.Они атомарны на уровне процессора и не требуют блокировки.Эти инструкции обычно работают только со словами платформы (обычно 32 или 64 бита памяти).
Вещи, которые вписываются в одно слово, такие как int
, byte
или ссылка на object
в куче, могут управляться атомарно.Вещи, которые не могут вписаться в одно слово, такие как struct
, как Nullable<decimal>
, или просто decimal
в этом отношении, не могут быть заменены атомарно.
Обходной путь долженпоменяйте местами объект, который ссылается на ваш decimal
(если он не нулевой) или просто на нулевое значение (если он нулевой).Этот object
создан для вас автоматически с помощью процесса, известного как упаковка и распаковка.
public volatile object myNullableDecimal = null;
И тогда в вашем коде вы можете сделать:
decimal? newValue = 34.3m;
decimal? oldvalue = (decimal?)Interlocked.Exchange(ref myNullableDecimal, newValue);
Вам придется явноприведите значения, хранящиеся в myNullableDecimal
, к decimal?
, чтобы использовать их, потому что упаковка происходит автоматически, а распаковка - нет.
Кроме того, не помещайте int
или что-либо, кроме Nullable<decimal>
или decimal
в myNullableDecimal, потому что хотя эти типы могут быть неявно преобразованы в Nullable<decimal>
(посредством неявного преобразования в decimal
) в штучной упаковке T:struct
можно только преобразовать в базовый T
.
object myObj = 23; // will work but myObj is now a boxed int, not a boxed decimal
var myDecimal = (decimal?) myObj; // throws an exception! Can't convert boxed ints to Nullable<decimal>
Из-за этих хитрых явных приведений я рекомендую вамОберните доступ к вашему «объекту», используя метод со встроенными приведениями. Этот метод будет работать для всех обнуляемых типов, а не только для десятичных.Он генерирует исключение приведения, если местоположение на самом деле не содержит правильного типа в штучной упаковке.Остерегайтесь, хотя: он все равно заменит старое значение перед выдачей исключения.Это означает, что это только атомно, когда это работает как ожидалось.Если произойдет сбой, он может произойти неэтомно.
public static T? ExchangeNullable<T>(ref object location, T? value) where T:struct
{
return (T?) Interlocked.Exchange(ref location, value);
}
«Более безопасный» метод, который заменяет только те значения, которые могут быть преобразованы в правильный возвращаемый тип, может выглядеть следующим образом.Этот метод является неблокирующим, атомарным и никогда не заменит старое значение, если это значение не может быть приведено к соответствующему типу.Но этот метод уязвим к истощению, поскольку поток может постоянно не обновлять значение, если оно изменяется чаще, чем время, необходимое для проверки успешности приведения.Чтобы бороться с этим, метод принимает необязательный CancellationToken
, чтобы позволить ему вызываться с таймаутом.Единственный способ избежать проблемы голодания - использовать блокировку (фактически справедливую блокировку, которая даже дороже, чем обычная блокировка).
Этот метод действительно полезен только в том случае, если вы не можете гарантировать, что объект не получит некоторые другие значения в нем, кроме коробочных типов соответствующего типа значения.Если вы управляете всем доступом к местоположению в своем собственном коде, это не должно быть проблемой, но поскольку компилятор позволяет вам вызывать эти методы для любой ссылки на объект (который может указывать практически на что угодно)обновление может завершиться неудачно, и этот метод гарантирует, что оно не будет выполнено атомарно.
public static T? ExchangeNullableSafe<T>(ref object location, T? value, CancellationToken token = default(CancellationToken)) where T : struct
{
// get the expected value
var expected = location;
while (true)
{
// check if the cast works
if (expected is T?)
{
// cast works, try the update. This includes a memory barrier so we can just do a normal read to
// populate the expected value initially.
var actual = Interlocked.CompareExchange(ref location, value, expected);
// check if the update worked
if (actual == expected)
{
// update worked. Break out of the loop and return
break;
}
else
{
// cast worked but the value was changed before the update occurred.
// update the expected value to the one the CompareExchange op gave us and try again.
// again, the memory barrier in the CompareExchange method guarantees that we are updating the expected value each time we run through the loop
expected = actual;
}
}
else
{
// the cast will fail. Just break out of the loop, try the cast, and let it fail.
break;
}
// since this method is vulnerable to starvation, we allow for cancellation between loops.
token.ThrowIfCancellationRequested();
}
// return the value or throw an exception
return (T?)expected;
}
Теперь все конвертируется автоматически, атомарно и без блокировки
object myNullableDecimal = null;
// ...
decimal? oldValue;
oldValue = ExchangeNullable<decimal>(ref myNullableDecimal, m4 + 7); // works and is atomic
// oldValue is an empty Nullable<decimal>, myNullableDecimal is a boxed 13m
oldValue = ExchangeNullable<decimal>(ref myNullableDecimal, 7.4m); // also works
// oldValue is a Nullable<decimal> with value 13m, myNullableDecimal is a boxed 7.4m
var oldValue = ExchangeNullable<decimal>(ref myNullableDecimal, null); // yep, works too
// oldValue is a Nullable<decimal> with value 7.4m, myNullableDecimal is null