Search code examples
c#reflectionatomic

Atomically reading values of fields via reflection


Let's suppose I have the following C# declaration:

struct Counters
{
    public long a;
    public long b;
    public long c;
}

Is this possible to iterate through the fields of a given instance of Counters and read their values using Interlocked.Read()? I.e.

Counters counters;
foreach (var counter in typeof(Counters).GetFields())
{
    var value = Interlocked.Read(???);
}

Solution

  • You can't use Interlocked.Read directly, because it requires a ref argument - and you can't use the required System.Int64& type directly.

    So, back to reflection:

    // You can keep this static in some helper class
    var method = typeof(Interlocked).GetMethod("Read", new []{ typeof(long).MakeByRefType() });
    
    var result = (long)method.Invoke(null, new object[] { counter.GetValue(instance) });
    

    EDIT: This doesn't work either, I botched up my testing. You're still reading a copy that wasn't produced atomically.

    This will work, though:

    public delegate long AtomicReadDelegate<T>(ref T instance);
    
    public static AtomicReadDelegate<T> AtomicRead<T>(string name)
    {
      var dm = new DynamicMethod(typeof(T).Name + "``" + name + "``AtomicRead", typeof(long), 
                                 new [] { typeof(T).MakeByRefType() }, true);
    
      var il = dm.GetILGenerator();
    
      il.Emit(OpCodes.Ldarg_0);
      il.Emit(OpCodes.Ldflda, typeof(T).GetField(name));
      il.Emit(OpCodes.Call, 
         typeof(Interlocked).GetMethod("Read", new [] { typeof(long).MakeByRefType() }));
    
      il.Emit(OpCodes.Ret);
    
      return (AtomicReadDelegate<T>)dm.CreateDelegate(typeof(AtomicReadDelegate<T>));
    }
    
    private readonly AtomicReadDelegate<Counters>[] _allTheReads = 
      new []
      {
        AtomicRead<Counters>("a"),
        AtomicRead<Counters>("b"),
        AtomicRead<Counters>("c")
      };
    
    public static void SomeTest(ref Counters counters)
    {
      foreach (var fieldRead in _allTheReads)
      {
        var value = fieldRead(ref counters);
    
        Console.WriteLine(value);
      }
    }
    

    You might want to cache the delegates you get from AtomicRead - they can be reused safely. A simple concurrent dictionary will work just fine.

    Don't forget that this only supports longs; you'll need to use Interlocked.CompareExchange if you need to atomically read other types as well (apart from references and ints, of course - though depending on your code, you might need some memory barriers even in that case).