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 Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s