1

Background

My colleague thinks reads in multithreaded C# are reliable and will always give you the current, fresh value of a field, but I've always used locks because I was sure I'd experienced problems otherwise. I spent some time googling and reading articles, but I mustn't be able to provide google with correct search input, because I didn't find exactly what I was after.

So I wrote the below program without locks in an attempt to prove why that's bad.

Question

I'm assuming the below is a valid test, then the results show that the reads aren't reliable/fresh.

Can someone explain what this is caused by? (reordering, staleness or something else)?

And link me to official Microsoft documentation/section explaining why this happens and what is the recommended solution?

If the below isn't a valid test, what would be?

Program

If there are two threads, one calls SetA and the other calls SetB, if the reads are unreliable without locks, then intermittently Foo's field "c" will be false.

using System;
using System.Threading.Tasks;

namespace SetASetBTestAB
{
    class Program
    {
        class Foo
        {
            public bool a;
            public bool b;
            public bool c;

            public void SetA()
            {
                a = true;
                TestAB();
            }

            public void SetB()
            {
                b = true;
                TestAB();
            }

            public void TestAB()
            {
                if (a && b)
                {
                    c = true;
                }
            }
        }

        static void Main(string[] args)
        {
            int timesCWasFalse = 0;
            for (int i = 0; i < 100000; i++)
            {
                var f = new Foo();
                var t1 = Task.Run(() => f.SetA());
                var t2 = Task.Run(() => f.SetB());
                Task.WaitAll(t1, t2);
                if (!f.c)
                {
                    timesCWasFalse++;
                }
            }
            Console.WriteLine($"timesCWasFalse: {timesCWasFalse}");
            Console.WriteLine("Finished. Press Enter to exit");
            Console.ReadLine();
        }
    }
}

Output

Release mode. Intel Core i7 6700HQ:

Run 1: timesCWasFalse: 8

Run 2: timesCWasFalse: 10

camios
  • 182
  • 13
  • Well, this is why the `volatile` keyword exists. If they were always "fresh", there would be no need for that keyword. The issue is that threads can re-order instructions, and that reordering can break things unexpectedly. –  Mar 10 '18 at 00:04
  • See https://stackoverflow.com/questions/154551/volatile-vs-interlocked-vs-lock –  Mar 10 '18 at 00:05
  • `volatile` isn't actually enough for this example, you need a full fence (for instance, using `Thread.MemoryBarrier()`) at the beginning of `TestAB` – Kevin Gosse Mar 10 '18 at 12:44
  • Hi @KevinGosse, you are correct, volatile doesn't fix the problem. Can you explain in details what is causing 'c' to be false and why a full fence is needed? Thanks! – camios Mar 12 '18 at 00:28
  • @camious: Because Race Conditions are race conditions. How stale or fresh the values you use to create them are does not mater. https://en.wikipedia.org/wiki/Race_condition#Software Volatile only deals with certain Optimisations (JiT and CPU) not **adding** issues to your worries. You still ahve to prevent Race Conditions on top of that. – Christopher Mar 12 '18 at 01:26
  • Kevin said fix by MemoryBarrier. If the statements are reordered, then why would I get intermittent behaviour? Christopher said: CPU caching (how is this solved?). speculates about statement optimisations by the JIT compiler (or CPU) - can this be proven here? Finally, says race condition. If variables update across the threads simultaneously and instructions are run in the same order as the c# code, there shouldn't be a race condition. To get a race condition the only thing that makes sense to me is when a thread reads stale value – camios Mar 12 '18 at 07:57
  • Or it could be statement reordering and race condition... – camios Mar 12 '18 at 08:40
  • 1
    @camios There are actually two distinct issues: consistency and freshness. Volatile guarantees you that you'll observe the operations in the expected order, but not that the cache will be up-to-date. Imagine you're writing 1 to `a` then `b`. Without volatile, you may observe any combination because of ordering (a=0 / b=0, a=1 / b=0, a=1 / b=1, and... a=0 / b=1). Volatile prevents reordering, and therefore ensures that you won't see a=0 / b=1. But other threads may still see a=0/b=0. `Thread.MemoryBarrier()` has the additional guarantee of flushing the cache. Same for `Interlocked` or `lock` – Kevin Gosse Mar 12 '18 at 18:49
  • That's actually a fun test to do to understand the side-effects of `Interlocked` or `lock`. Add a `public int d;` field in your class, then call `Interlocked.Increment(ref d);` at the beginning of `TestAB`. You'll see that the freshness issue is fixed, even though you didn't change anything about `a` or `b` – Kevin Gosse Mar 12 '18 at 18:52

1 Answers1

2

Of course it is not fresh. The average CPU nowadays has 3 layers of Caches between each cores Registers and the RAM. And it can take quite some time for a write to one cache to be propagate to all of them.

And then there is the JiT Compiler. Part of it's job is dead code dection. And one of the first things it will do is cut out "useless" variables. For example this code tried to force a OOM excpetion by running into the 2 GiB Limit on x32 Systems:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace OOM_32_forced
{
    class Program
    {
        static void Main(string[] args)
        {
            //each short is 2 byte big, Int32.MaxValue is 2^31.
            //So this will require a bit above 2^32 byte, or 2 GiB
            short[] Array = new short[Int32.MaxValue];

            /*need to actually access that array
            Otherwise JIT compiler and optimisations will just skip
            the array definition and creation */
            foreach (short value in Array)
                Console.WriteLine(value);
        }
    }
}

The thing is that if you cut out the output stuff, there is a decent chance that the JiT will remove the variable Array inlcuding the instantionation order. The JiT has a decent chance to reduce this programming to doing nothing at all at runtime.

volatile is first preventing the JiT from doing any optimisations on that value. And it might even have some effect on how the CPU processes stuff.

Christopher
  • 9,634
  • 2
  • 17
  • 31
  • 1
    You can disable the JIT optimization by marking the method with `[MethodImpl(MethodImplOptions.NoOptimization)]` attribute. I think this answer could benefit with a discussion on how `lock` doesn't mean you are getting the "freshest" value either. – Ron Beyer Mar 10 '18 at 00:59
  • How is the Jit compiler optimising away useless variables relevan to this question? – camios Mar 10 '18 at 21:24
  • @camios: Because the variable you think you are accessing might not actually *exist* at runtime. It might be replaced with the function/property call that was originally used to fill the value. A temp variable you think protects you against race conditions (because you work with a copy) could be optimised away. See here for example: https://stackoverflow.com/a/17161931/3346583 – Christopher Mar 10 '18 at 21:57
  • Oh, and just another case where it maters: Array Bound Check Elimination. Case "Redundant array accesses". It might actually create a temproary variable of sorts instead of doing the same array access twice: https://blogs.msdn.microsoft.com/clrcodegeneration/2009/08/13/array-bounds-check-elimination-in-the-clr/ Trying to get a fresh value from a copy is kind of a no-go. Never underestimate how much the JiT can screw with your asumptions about what is executed! Luckily volatile should take care of this too. – Christopher Mar 11 '18 at 03:48
  • @Christopher, putting volatile on 'a' and 'b' doesn't fix the problem. – camios Mar 12 '18 at 00:33