It's dangerous to code alone! Take this.

Locks and Reading Data

In The C# Player’s Guide, in the Threads level (Level 43), there is a section called Thread Safety that shows a scenario where two threads update a single variable by using ++, and illustrates a concurrency problem if the two threads try to update the variable at the same time. The book then shows how you can use a lock statement to prevent multiple threads from modifying the variable at the same time, fixing the concurrency problem.

There’s one catch that the book doesn’t really get into, that I want to cover in this article.

As a general policy, you actually want to protect both reads and writes to the data with a lock or similar tool.

Let’s suppose you have the following class:

public class TwoOppositeNumbers
{
    private int _primary;
    private int _opposite;

    public int Sum => _primary + _opposite;

    public void Add(int amount)
    {
        _primary += amount;
        _opposite -= amount;
    }
}

Because _opposite subtracts the same amount _primary adds, the two should always be opposites (+2 and -2 or -11 and +11, for example). That also means Sum should always return 0.

If an instance class is only ever used by a single thread, you’ll be fine without any additional work.

But if two threads are using an instance of this class, you will need to use a lock to ensure you don’t end up with any inconsistencies.

If you’re just looking at the sample in the book, you may come to the conclusion that you need to put a lock into the Add method:

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

    private int _primary;
    private int _opposite;

    public int Sum => _primary + _opposite;

    public void Add(int amount)
    {
        lock (_lockObject)
        {
            _primary += amount;
            _opposite -= amount;
        }
    }
}

This step is necessary, but it may not be enough. This ensures that if the two threads both call Add, when they’re both done, both additions will have correctly taken place.

But where this potentially falls short is if a thread reads the data while another thread writes the data.

Let’s walk through an illustration.

Suppose an instance of TwoOppositeNumbers has the values _primary=0 and _opposite=0. Thread #1 calls Add(5). By the time it finishes, we would expect _primary to have a value of 5 and _opposite to have a value of -5, and Sum should be 0.

But suppose Thread #2 calls Sum while Thread #1 is still working through the code in the lock statement. It is possible for _primary to be updated to 5 while _opposite is still 0, giving us a result of 5 instead of the expected 0.

To fix this, we can also put a lock around the places where we read _primary and _opposite, and not just the places where we write new values to those variables. Here, that is achieved by also putting a lock statement into the Sum property:

public int Sum
{
    get
    {
        lock (_lockObject)
        {
            return _primary + _opposite;
        }
    }
}

As a general rule, if you need to ensure something is thread safe, you will want to put a lock around all places that it is accessed, regardless of if that is a read or a write.

In this sense, the book’s solution isn’t totally correct because it doesn’t bother locking when reading the data. But I don’t think you’ll find any actual concurrency problems for that specific situation.

See, the time you run into trouble is when you have a chance at reading the data while it has only partially been changed. In the example above, there are two variables that are updated at different times. Without proper locking, the reader thread may see the two variables in an inconsistent state, as it is in the process of changing.

In the book, there is only one variable in the code snippet, and it is one that is guaranteed to be updated all at once.

Using fancier words, this is called being atomic, meaning it is indivisible and has either completely not happened or completely happened. There is no “partially happened” state.

When you can get all the data you need in an atomic fashion, you don’t technically need the lock. You will either see it in the before state or the after state, but not an “in transition” state.

In our example above, because there are two variables, the updates and reads won’t be atomic, so we need the lock to ensure we don’t ever accidentally see the data in that intermediate, inconsistent state.

It is also notable that not all variable types are atomic. That is, it doesn’t always require multiple variables to have issues. The rules are rather technical (and more detail than I want to get into in this article) but in short, if a value type is bigger than the “native” size of things on your computer, it can’t be atomic. On a 32-bit computer, that means long, ulong, double, and decimal can’t be atomic, but must be read in two parts. (Though on 64-bit computers, long, ulong, and double may be atomic.) And, of course, a struct or a class with multiple parts to it will not be atomic. You can’t just assume that because it only deals with a single variable, it is atomic, because while that is sometimes true, it also often isn’t.

Bottom line: reading and writing usually both require locking. The sample in the book is only able to get away without the check on reading because it happens to be atomic.