简体   繁体   中英

Interlocked.Exchange nullable decimal

I want to exchange two nullable decimal values, like this:

o2 = Interlocked.Exchange(ref o1, o2);

The type 'decimal?' must be a reference type in order to use it as parameter 'T' in the generic type or method 'System.Threading.Interlocked.Exchange(ref T, T)'.

Is there better idea than this:

decimal? temp = o1;
o1 = o2;
o2 = temp;

Thanks in advance!

Two thoughts:

  • treat it as object and cast at the consumer
  • create a Box<T> class where T:struct (and make it immutable), and swap some Box<decimal> references

In both cases, the consumer should take a clone of the value before anything else (no double reads; it may change between reads).

Interlocked.Exchange attempts to use the CPU's atomic instructions on whatever platform you are running on. These are atomic at the CPU level and require no locking. These instructions typically only work on platform words (usually 32 or 64 bits of memory).

Things that fit into a single word, like a int , a byte , or a reference to an object on the heap can be manipulated atomically. Things that can't fit into a single word, such as a struct like Nullable<decimal> , or just a plain decimal for that matter, can't be swapped atomically.

A workaround is to swap an object that references your decimal (if it's non-null) or just a null value (if it is null). This object is created for you automatically using a process known as boxing and unboxing.

public volatile object myNullableDecimal = null;

And then in your code you can do:

decimal? newValue = 34.3m;
decimal? oldvalue = (decimal?)Interlocked.Exchange(ref myNullableDecimal, newValue);

You will have to explicitly cast the values stored in myNullableDecimal to decimal? to use them because boxing is automatic, but unboxing is not.

Also, don't put an int or anything but a Nullable<decimal> or decimal in myNullableDecimal because although a these types can be implicitly converted to a Nullable<decimal> (via implicit conversion to a decimal ) a boxed T:struct can only be converted to the underlying 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>

Because of these tricky explicit casts, I recommend you wrap access to your 'object' using a method with the casts built in. This method will work for all nullable types, not just decimals. It throws a cast exception if the location doesn't actually contain the proper boxed type. Watch out though: it will still replace the old value before throwing the exception. That is to say that it is only atomic when it works as expected. If it fails, it might fail un-atomically.

public static T? ExchangeNullable<T>(ref object location, T? value) where T:struct
{
    return (T?) Interlocked.Exchange(ref location, value);
}

A "safer" method that only replaces values that can be cast to the proper return type might look like the following. This method is non-blocking, atomic, and will never replace the old value if that value cannot be cast to the appropriate type. But this method is vulnerable to starvation since a thread might perpetually fail to update the value if it changes more frequently than the time it takes to verify the cast will succeed. To combat this, the method takes an optional CancellationToken to allow it to be called with a timeout. The only way to avoid the starvation problem is by using locking (actually fair locking, which is even more expensive than regular locking).

This method is really only useful if you can't guarantee that the object won't get some other values places in it besides boxed types of the appropriate value type. If you are controlling all the access to the location in your own code this shouldn't be an issue, but since the compiler lets you call these methods on any object reference (which might point to just about anything), the update could fail and this method guarantees it fails atomically.

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

Now everything converts automatically, atomically, and without blocking

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM