Home > Back-end >  What happens when you read and write an object from multiple threads without protection?
What happens when you read and write an object from multiple threads without protection?

Time:12-10

I have learned that accessing the same Object from different Threads is not threadsafe and should be protected. Be it thru Locking or 'Interlocked.Exchange' or Immutables or any other means. This means that the following code is potentially NOT threadsafe because it does not protect access to the shared 'test' object.

My Questions are:

  1. Is the following code Safe or Not?
  2. If Not, what is the worst that could happen?
  3. If Not, is there any exception that is thrown on a dirty Read or Writes that I could catch to prevent the worst?
class Test
{
    public Test()
    {
        Foo =new Random().Next(10000);
    }

    public int Foo;
}

internal class Program
{
    public static async Task Main(string[] args)
    {
        var test =new Test();


        var exitToken = new CancellationTokenSource(TimeSpan.FromSeconds(120)).Token;

        var readerTask = Task.Run(async () =>
        {
            while (!exitToken.IsCancellationRequested)
            {
                Console.WriteLine("Random Foo: "   test.Foo);

                await Task.Delay(TimeSpan.FromSeconds(5));
            }
        });


        var writerTask = Task.Run(async () =>
        {
            while (!exitToken.IsCancellationRequested)
            {
                test = new Test();

                await Task.Delay(TimeSpan.FromSeconds(5));
            }
        });


        await Task.WhenAll(readerTask, writerTask);
    }
}

CodePudding user response:

Is the following code Safe or Not?

When talking about "safe" it is important to specify what it is "safe" for. Usually when designing a class we have some requirements we want the class to fulfill (sometimes called "invariants"). If these requirements are fulfilled even if the object is used from multiple threads we call it "thread safe". Take a trivial class:

public class Incrementer{
  public int Value {get; private set;}
  public void Increment() => Value  ;
}

We would probably have a requirement that the value should exactly equal the number of times the Increment method was called. This requirement would not be fulfilled if it is called concurrently, but that does not mean it will throw an exception, blow up, or crash, just that the value does not match our requirement.

If we change the requirement to be "value > 0 if Increment is called at least one time", then the read-modify-write issue is not relevant, since we only care if the value has been written once.

In your specific example the only variable that is written to and read concurrently is the test-variable. Since writing references are atomic we know this will always point to some object. There could potentially be issues with reordering when the object is constructed. This could cause the reference to be updated before the actual value has been set, and would therefore be observed as zero. I do not think this would occur with common x86 hardware and software, but the safe version would be to either use a lock, or create the object, issue a memory barrier and then update the reference.

The other potential risk is that the read-thread does not update the reference from memory, and just loads it once and reuses the same value in each iteration. I do not think this could actually occur in this case, since the loop also calls Task.Delay and Console.WriteLine, and I would expect both of these to issue a memory barriers or something else that ensures a read actually occurs, and that no reordering is done. But I would probably still recommend using a lock or marking the variable as volatile, it is usually a good idea to err on the side of caution.

If Not, what is the worst that could happen?

In this case, that the same value would always be printed. But as I mentioned above, this will most likely not occur.

If Not, is there any exception that is thrown on a dirty Read or Writes that I could catch to prevent the worst?

In general, no. Some types may throw exceptions if they are used concurrently or from the wrong thread, a typical example would be any UI classes. But this is not something that should be relied upon.

This is also one of the reason why multi threading bugs are so devious, If you are unlucky the only effect is that the value is wrong, and it might not even be obviously wrong, it might just be off a little bit. And if you are even more unlucky it only occur in special circumstances, like when running under full load on a 20 core server, so you might never be able to reproduce it in a development environment.

  • Related