Simple thread-safe class

Let’s write thread-safe class with two properties:
– logical property “State” describes state of the object;
– numeric property “Count” counts how many relative objects including current one has TRUE state.
First approach gives us the following class:

public class Data
{
  private bool _state;
  private int _count;

  public bool State
  {
    get { return _state; }
    set
    {
      if (_state == value)
        return;

      _state = value;
      Count += (_state ? 1 : -1);
    }
  }

  public int Count
  {
    get { return _count; }
    set
    {
      if (value < 0)
        throw new ArgumentOutOfRangeException(
          nameof(Count),
          "Count should be positive");
      _count = value;
    }
  }
}

Full code is accessible on GitHub Blog repository.

This class is not thread-safe that could be easily proved by the following test:

[Fact]
public void Data_ShouldThrow_ExceptionOnNegativeStateInTwoAsyncTasks()
{
  var data = new Data();
  var tasks = new[]
    {
      Task.Run(() =>
        {
          for (var pos = 0; pos < 500000; pos++)
          {
            data.State = (pos % 2 == 0);
          }
        }),
      Task.Run(() =>
        {
          for (var pos = 0; pos < 500000; pos++)
          {
            data.Count += (pos % 2 == 0 ? 1 : -1);
          }
      }),
    };

  var exception = Record.ExceptionAsync(async () =>
        await Task.WhenAll(tasks));
  data.Should().NotBeNull();
  exception.Should().NotBeNull();
  exception.Result.Should().NotBeNull();
  exception.Result.Message.Should().Be(ExceptionMessage);
}

It is successful if an exception about negative count is throw. The following schema describes possible statements that generate an exception:

01. Simultaneous
Simultaneous access to class’ fields

One of the approaches to convert class to thread-safe class is to use Monitor, Mutex or lock statement for access to properties. Let’s use lock statement in the second version of class. Class contains private field _lockObject that is used by lock inside getters and setters:

public class ThreadSafeData
{
  private readonly object _lockObject = new object();

  private bool _state;
  private int _count;

  public bool State
  {
    get
    {
      lock (_lockObject)
      {
        return _state;
      }
    }
    set
    {
      lock (_lockObject)
      {
        if (_state == value)
          return;

        _state = value;
        Count += (_state ? 1 : -1);
      }
    }
  }

  public int Count
  {
    get
    {
      lock (_lockObject)
      {
        return _count;
      }
    }
    set
    {
      lock (_lockObject)
      {
        if (value < 0)
          throw new ArgumentOutOfRangeException
            (nameof(Count),
            "Count should be positive");
        _count = value;
      }
    }
  }
}

Now State setter is thread-safe. Lock prevents simultaneous access to Count getter, and other threads are blocked until setter is executed completely. But unfortunately it is not enough, and the mentioned test is still successful. Some speculations lead us to the following timeline:

02. State lock
State setter is thread-safe

It means that statement this.Count += 1 should be converted to thread-safe operation. As it should use the same _lockObject, we may create new method AddCount:

public int AddCount(int delta)
{
  lock (_lockObject)
  {
    Count += delta;
    return Count;
  }
}

The following test cover this method:

[Fact]
public void ThreadSafeData_ShouldNotThrow_ExceptionInTwoAsyncTasks()
{
  var data = new ThreadSafeData();
  var tasks = new[]
    {
      Task.Run(() =>
        {
          for (var pos = 0; pos < 500000; pos++)
          {
            data.State = (pos % 2 == 0);
          }
        }),
      Task.Run(() =>
        {
          for (var pos = 0; pos < 500000; pos++)
          {
            data.AddCount (pos % 2 == 0 ? 1 : -1);
          }
      }),
    };

  var exception = Record.ExceptionAsync(async () =>
        await Task.WhenAll(tasks));
  data.Should().NotBeNull();
  exception.Should().NotBeNull();
  exception.Result.Should().BeNull();
}

1. All used IP-addresses, names of servers, workstations, domains, are fictional and are used exclusively as a demonstration only.
2. Information is provided «AS IS».

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.