Skip to content

[API Proposal]: Volatile.(Read|Write)Enum #94727

Open
@colejohnson66

Description

@colejohnson66

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions