-
Notifications
You must be signed in to change notification settings - Fork 5.2k
Description
Background and motivation
Normally, one would use Volatile.Read and Volatile.Write to handle interlocking of numeric values between tasks. Sometimes it's beneficial to use enums in these cases, but we are prevented from doing so due to limitations in the API. Currently, one must cast their enum values back and forth when reading and writing:
MyEnum nextState = default;
Task task = Task.Run(() =>
{
...
Volatile.Write(ref nextState, success ? (int)MyEnum.SuccessState : (int)MyEnum.FailureState);
});
...
task.Wait();
MyEnum newState = (MyEnum)Volatile.Read(ref nextState);Having overloads that take enums would be beneficial in these cases to avoid issues that occur when casting enum values (such as using incorrect numeric or enum types). For example, a ulong backed enum must be cast to either long or ulong lest you lose 32 bits of data. Casting to int above would throw away such data.
API Proposal
namespace System.Threading;
public static class Volatile
{
public static TEnum ReadEnum<TEnum>(ref TEnum location)
where TEnum : struct, Enum;
public static void WriteEnum<TEnum>(ref TEnum location, TEnum value)
where TEnum : struct, Enum;
}In each function, sizeof(TEnum) would be used to determine the size of the enum. Then Unsafe.As and normal Volatile methods would be used to massage out the value. For example, an implementation of ReadEnum that only handled single-byte-backed enums would look like
public static TEnum ReadEnum<TEnum>(ref TEnum location)
where TEnum : struct, Enum
{
if (sizeof(TEnum) == sizeof(byte))
{
byte value = Volatile.Read(ref Unsafe.As<TEnum, byte>(ref location));
return Unsafe.As<byte, TEnum>(ref value);
}
throw new NotSupportedException("Unsupported enum underlying type.");
}API Usage
MyEnum nextState = default;
Task task = Task.Run(() =>
{
...
Volatile.WriteEnum(ref nextState, success ? MyEnum.SuccessState : MyEnum.FailureState);
});
...
task.Wait();
MyEnum newState = Volatile.ReadEnum(ref nextState);Alternative Designs
Alternatively, Volatile.Read<T>(ref T) and Volatile.Write<T>(ref T, T) can have their generic constraint removed and replaced with runtime checks (a la #99205 and #100842):
namespace System.Threading;
public static class Volatile
{
public static T Read<T>(ref T location)
/* where T : class */;
public static void Write<T>(ref T location, T value)
/* where T : class */;
}If T : class, it operates the same. However, if T : struct, Enum, it would now operate as proposed. This is a non-breaking change, as it relaxes a generic constraint to allow previously non-compilable code.
It could be extended even further to work on any unmanaged struct that's eight bytes or fewer. In other words, Volatile.Read(ref myFourByteStruct) would now be legal.
This alternate design is probably undesirable. Despite being non-breaking, it can be risky. Say I have a four-byte struct and use Volatile.(Read|Write) on it. If, in the future, I change it to be, say, 12 bytes large, the code will still compile without warnings, but would always throw at runtime. If this alternate design is chosen, a source analyzer should be included. It would detect T types that are greater than eight bytes and raise a compile-time warning.