I have a bit of my game which looks like this:
public static float Time;
float someValue = 123;
Interlocked.Exchange(ref Time, someValue);
I want to change Time to be a Uint32; however, when I try to use UInt32
instead of float
for the values, it protests that the type must be a reference type. Float
is not a reference type, so I know it's technically possible to do this with non-reference types. Is there any practical way to make this work with UInt32
?
[edit:] Mea culpa and apologies to @AnorZaken since my answer is similar to his. I honestly didn't see it before posting mine. I'll keep this for now in case my text and explanations are useful or have additional insights, but credit for prior work properly goes to Anor.
Although I have another solution on this page, some people might be interested in a totally different approach. Below, I give a
DynamicMethod
which implementsInterlocked.CompareExchange
for any 32- or 64-bit blittable type, which includes any customEnum
types, the primitive types that the built-in method forgot (uint
,ulong
), and even your ownValueType
instances--so long as any of these are dword (4-bytes, i.e.,int
,System.Int32
) or qword (8-bytes,long
,System.Int64
) sized. For example, the followingEnum
type won't work since it specifies a non-default size,byte
:As with most DynamicMethod implementations of runtime-generated IL, the C# code isn't beautiful to behold, but for some people the elegant IL and sleek JITted native code make up for that. For example, in contrast to the other method I posted, this one doesn't use
unsafe
C# code.To allow automatic inference of the generic type at the call site, I wrap the helper in a
static
class:Technically, the above is all you need. You can now call
CmpXchgIL<T>.CmpXchg(...)
on any appropriate value type (as discussed in the intro above), and it will behave exactly like the built-inInterlocked.CompareExchange(...)
inSystem.Threading
. For example, lets say you have astruct
containing two integers:You can now atomically publish the 64-bit struct just as you would expect with any CmpXchg operation. This atomically publishes the two integers so that it is impossible for another thread to see a 'torn' or inconsistent pairing. Needless to say, easily doing so with a logical pairing is hugely useful in concurrent programming, even more so if you devise an elaborate struct that packs many fields into the available 64 (or 32) bits. Here's an example of the call-site for doing this:
Above, I mentioned that you can tidy up the call site by enabling type inference so that you don't have to specify the generic parameter. To do this, just define a static generic method in one of your non- generic global classes:
I'll show the simplified call site with a different example, this time using an
Enum
:As for the original question,
ulong
anduint
work trivially as well:You cannot pass a casted expression by reference, you should use a temporary variable:
Although ugly, it is actually possible to perform an atomic Exchange or CompareExchange on an enum or other blittable value type of 64 bits or less using
unsafe
C# code:The counterintuitive part is that the ref expression on the dereferenced pointer does actually penetrate through to the address of the enum. I think the compiler would have been within its rights to have generated an invisible temporary variable on the stack instead, in which case this wouldn't work. Use at your own risk.
[edit: for the specific type requested by the OP]
[edit: and 64-bit unsigned long]
(I also tried using the undocumented C# keyword
__makeref
to achieve this, but this doesn't work because you can't useref
on a dreferenced__refvalue
.It's too bad, because the CLR maps the[comment mooted by JIT interception, see below])InterlockedExchange
functions to a private internal function that operates onTypedReference
[edit: July 2018] You can now do this more efficiently using the System.Runtime.CompilerServices.Unsafe library package. Your method can use
Unsafe.As<TFrom,TTo>()
to directly reinterpret the type referenced by the target managed reference, avoiding the dual expenses of both pinning and transitioning tounsafe
mode:Of course this works for
Interlocked.Exchange
as well. Here are those helpers for the 4- and 8-byte unsigned types.This works for enumeration types also--but only so long as their underlying primitive integer is exactly four or eight bytes. In other words,
int
(32-bit) orlong
(64-bit) sized. The limitation is that these are the only two bit-widths found among theInterlocked.CompareExchange
overloads. By default,enum
usesint
when no underlying type is specified, soMyEnum
(from above) works fine.I'm not sure whether the 4-byte minimum is a fundamental to .NET, but as far as I can tell it leaves no means of atomically swapping (values of) the smaller 8- or 16-bit primitive types (
byte
,sbyte
,char
,ushort
,short
) without risking collateral damage to adjacent byte(s). In the following example,BadEnum
explicitly specifies a size that is too small to be atomically swapped without possibly affecting up to three neighboring bytes.If you're not constrained by interop-dictated (or otherwise fixed) layouts, a workaround would be to ensure that the memory layout of such enums is always padded to the 4-byte minimum to allow for atomic swapping (as
int
). It seems likely, however, that doing so would defeat whatever purpose there might have been for specifying the smaller width in the first place.[edit: April 2017] I recently learned that when
.NET
is running in 32-bit mode (or, i.e. in the WOW subsystem), the 64-bitInterlocked
operations are not guaranteed to be atomic with respect to non-Interlocked
, "external" views of the same memory locations. In 32-bit mode, the atomic guarantee only applies globablly across QWORD accesses which use theInterlocked
(and perhapsVolatile.*
, orThread.Volatile*
, TBD?) functions.In other words, to obtain 64-bit atomic operations in 32-bit mode, all accesses to those QWORD locations must occur through
Interlocked
in order to preserve the guarantees, and you can't get cute assuming that (e.g.) direct reads are protected just because you always useInterlocked
functions for writing.Finally, note that the
Interlocked
functions in theCLR
are specially recognized by, and receive special treatment in, the .NET JIT compiler. See here and here This fact may help explain the counter-intuitiveness I mentioned earlier.Perhaps use
int
instead ofuint
; there are overloads forint
. Do you need the extra bit of range? If so, cast / convert as late as possible.It is still a hack but it is possible to do this with IL-generation instead of using
unsafe
code. The benefit is that instead of relying on a compiler implementation detail it relies on the fact that the signed and unsigned types are of the same bit-length, which is part of the spec.Here is how:
Credit to "hvd" for the IL-generation idea and similar code for a CompareExchange method for Enums, which can be found here.
There will be some overhead for generating the method on the first call, but the generated method is stored in delegate form so any subsequent calls should be very performant.
And to quote from the above link:
There's an overload for
Interlocked.Exchange
specifically forfloat
(and others fordouble
,int
,long
,IntPtr
andobject
). There isn't one for uint, so the compiler reckons the closest match is the genericInterlocked.Exchange<T>
- but in that caseT
has to be a reference type.uint
isn't a reference type, so that doesn't work either - hence the error message.In other words:
Interlocked.Exchange(ref float, float)
.uint
fails because there's no applicable overload. The exact error message is caused by the compiler guessing that you meanInterlocked.Exchange<T>(ref T, T)
.As for what to do, the options are any of:
int
instead, as Marc suggests.long
.uint
but don't try to write lock-free codeAlthough obviously
Exchange
works fine with some specific value types, Microsoft hasn't implemented it for all the primitive types. I can't imagine it would have been hard to do so (they're just bits, after all) but presumably they wanted to keep the overload count down.