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?
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
unsafeC# code:The counterintuitive part is that the ref expression on the dereferenced pointer does actually penetrate through the cast 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
__makerefto achieve this, but this doesn’t work because you can’t userefon a dreferenced__refvalue.It’s too bad, because the CLR maps the[comment mooted by JIT interception, see below])InterlockedExchangefunctions 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 tounsafemode:Of course this works for
Interlocked.Exchangeas 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.CompareExchangeoverloads. By default,enumusesintwhen 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,BadEnumexplicitly 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
.NETis running in 32-bit mode (or, i.e. in the WOW subsystem), the 64-bitInterlockedoperations 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, including reads, must occur through
Interlocked/Volatilein order to preserve the guarantees, so you can’t get cute assuming (e.g.) that direct (i.e., non-Interlocked/Volatile) reads are protected just because you always useInterlocked/Volatilefunctions for writing.Finally, note that the
Interlockedfunctions in theCLRare 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.