Why does Interlocked.CompareExchange<T> only support reference types?
Asked Answered
R

3

7

Disclaimer: My posts are apparently always verbose. If you happen to know the answer to the title question, feel free to just answer it without reading my extended discussion below.


The System.Threading.Interlocked class provides some very useful methods to assist in writing thread-safe code. One of the more complex methods is CompareExchange, which can be used for computing a running total that may be updated from multiple threads.

Since the use of CompareExchange is a bit tricky, I thought it a rather common-sense idea to provide some helper methods for it:

// code mangled so as not to require horizontal scrolling
// (on my monitor, anyway)
public static double Aggregate
(ref double value, Func<double, double> aggregator) {
    double initial, aggregated;

    do {
        initial = value;
        aggregated = aggregator(initial);
    } while (
        initial != Interlocked.CompareExchange(ref value, aggregated, initial)
    );

    return aggregated;
}

public static double Increase(ref double value, double amount) {
    return Aggregate(ref value, delegate(double d) { return d + amount; });
}

public static double Decrease(ref double value, double amount) {
    return Aggregate(ref value, delegate(double d) { return d - amount; });
}

Now, perhaps I am just guilty of being generic-happy (I will admit, this is often true); but it does feel silly to me to restrict the functionality provided by the above methods to double values only (or, more accurately, for me to have to write overloaded versions of the above methods for every type I want to support). Why can't I do this?

// the code mangling continues...
public static T Aggregate<T>
(ref T value, Func<T, T> aggregator) where T : IEquatable<T> {
    T initial, aggregated;

    do {
        initial = value;
        aggregated = aggregator(initial);
    } while (
        !initial.Equals(
            Interlocked.CompareExchange<T>(ref value, aggregated, initial)
        )
    );
}

I can't do this because Interlocked.CompareExchange<T> apparently has a where T : class constraint, and I don't understand why. I mean, maybe it's because there are already overloads for CompareExchange that accept Int32, Int64, Double, etc.; but that hardly seems a good rationale. In my case, for example, it would be quite handy to be able to use the Aggregate<T> method to perform a wide range of atomic calculations.

Retroflex answered 11/2, 2010 at 1:6 Comment(0)
Y
12

Interlocked.CompareExchange is meant to be implemented with native atomic instructions provided directly by the processor. It's pointless to have something like that use a lock internally (it's designed for lock-free scenarios).

Processors that provide atomic compare exchange instruction naturally support it as small, "register-sized" operations (e.g. the largest compare-exchange instruction on an Intel x64 processor is cmpxchg16b that works on 128 bit values).

An arbitrary value type can be potentially bigger than that and compare-exchanging it may not be possible with a single instruction. Compare-exchanging a reference type is easy. Regardless of its total size in memory, you'll be comparing and copying a small pointer of a known size. This is also true for primitive types like Int32 and Double—all of them are small.

Year answered 11/2, 2010 at 1:11 Comment(8)
I think this snapped into focus for me once I read this in the MSDN documentation shortly after posting the question (for the overload that accepts Object parameters): "The objects are compared for reference equality, rather than Object.Equals. As a result, two boxed instances of the same value type (for example, the integer 3) always appear to be unequal, and no operation is performed. Do not use this overload with value types." Of course--it's not going to use the Equals method, but rather check the references themselves.Retroflex
So clearly my only choice would in fact be to write all of those overloaded methods myself (for int, long, etc.) after all -- if I really wanted to go that route. Right?Retroflex
@Dan: I'm not sure how you'd do that for a larger value type atomically without using a lock (swapping a bunch of integer values). The point of compare-exchange is that it's atomic; that is, there will not be a point in time when you can observe one part of the operation is done and the other part is not. Putting a bunch of compare-exchanges along each other won't help making the overall operation atomic (each part is atomic, but the operation as a whole, is not).Year
@Dan: Ignore my last comment. Your disclaimer made me not read your question and I misunderstood what you meant from an overloaded method ;). Yes, you'll have to do so.Year
@Mehrdad: I think you lost me. My intention wasn't to use "a bunch of compare-exchanges along each other"... rather to provide an easy way to perform any kind of single aggregation atomically (e.g., summing up squared values).Retroflex
@Mehrdad: OK, ignore my last comment.Retroflex
@Dan: Yep. I said that just before your comment. You'll have to implement it specifically for int, long, ... either using overloads or by checking typeof(T) and taking an alternate code path. You won't be able to do this with a single code path.Year
I wonder if there would have been any difficulty of providing a struct PairStruct<T1,T2> where T1:class where T2:class {public T1 f1; public T2 f2;} and providing an overload of CompareExchange for that, since processors where that type would take 64 bits have 64-bit compare-exchange instructions, and those where the type would take 128 bits have 128-bit compare-exchange instructions? Being able to swap two references at once would be rather handy sometimes.Warthman
L
1

Because that overload is specifically intended to compare and exchange a reference. It does not perform an equality check using the Equals() method. Since a value type would never have reference equality with the value you're comparing it against, my guess is that they constrained T to class in order to prevent misuse.

Lasky answered 11/2, 2010 at 1:13 Comment(3)
I bet your last name gives you a lot of cred among your coworkers.Retroflex
That was a +1 from me, by the way; I wasn't being snide.Retroflex
LOL it's okay, I didn't take it as such. Very good question too by the way.Lasky
B
-1

I would suspect that Interlocked.CompareExchange<T> just performs an atomic pointer-swap under the hood.

Trying to do that with a value type would likely not give the results you expect.

You could, of course, box value types up in an object before using them.

Bigoted answered 11/2, 2010 at 1:9 Comment(1)
Turns out boxing the value types as object would not work, actually. See my first comment to Mehrdad's answer.Retroflex

© 2022 - 2024 — McMap. All rights reserved.