Home > database >  Interlocked update of field in struct vs class
Interlocked update of field in struct vs class

Time:06-14

Background

I've have a stats object that has a number of counters as properties. The counters can be updated anywhere in the application. The stats object is a singleton class (which used to have public fields of type long and int). The fields were updated using Interlocked.Increment.

I changed the fields to properties and created a new class called InterlockedInt64, to encapsulate the call to Increment. I copy the previous stats object to a temporary filed in order to track a delta. I use the Clone method to do that since it is a class.

The simplified code is here: https://dotnetfiddle.net/baEm8E (and below).

Problem

If the InterlockedInt64 was a struct, I wouldn't need the Clone method. But if I change it to a struct, the Increment doesn´t work anymore.

Can someone explain the mechanics behind? I just need to understand why using long wrapped in a custom struct isn't updated, but a long field is. I understand the difference between struct and class and all why using a class works.

using System;
using System.Globalization;
using System.Threading;

public class Program
{
    public static void Main()
    {
        var test = new Stats();
        
        test.Counter.Increment();
        Console.WriteLine($"Expected 1, got {test.Counter}");
        test.Counter.Increment();
        Console.WriteLine($"Expected 2, got {test.Counter}");
        test.Counter.Increment();
        Console.WriteLine($"Expected 3, got {test.Counter}");
    }

    public class Stats
    {
        public InterlockedInt64 Counter { get; init; } = new InterlockedInt64();
    }
    
    //public class InterlockedInt64 // Works
    public struct InterlockedInt64 // Does not work
    {
        private long _value;
        public InterlockedInt64()
        {
            _value = 0;
        }

        public InterlockedInt64(long value)
        {
            _value = value;
        }

        public long Increment() => Interlocked.Increment(ref _value);
        public long Decrement() => Interlocked.Decrement(ref _value);
        public long Exchange(long newValue) => Interlocked.Exchange(ref _value, newValue);
        public InterlockedInt64 Clone() => new(_value);
        
        public static implicit operator InterlockedInt64(long v)
        {
            return new InterlockedInt64(v);
        }

        public static implicit operator long (InterlockedInt64 v)
        {
            return v._value;
        }

        public override string ToString() => _value.ToString(CultureInfo.CurrentCulture);
        public string ToString(CultureInfo cultureInfo) => _value.ToString(cultureInfo);
    }
}

edit: Removed some code and renamed Test -> Stats

CodePudding user response:

When working with struct, you have to deal with their copies (struct are passed by values, not by reference):

// you get a copy of test.Counter which you Increment and then throw it away
// note, that original test.Counter (its backing field) is not changed
test.Counter.Increment();
// you get a copy of test.Counter (which is unchanged) and print it out
Console.WriteLine($"Expected 1, got {test.Counter}");

If you insist on working with InterlockedInt64 being struct, you can try dealing with references to struct:

public class Test {
  // Backing field
  private InterlockedInt64 m_Counter = new InterlockedInt64();

  // we want to return reference (not copy) of the m_Counter
  public ref InterlockedInt64 Counter {
    get {
      // and we return reference to the original field (not its copy)
      return ref m_Counter;
    }
  }
}
  • Related