13

If I understand meaning of volatile and MemoryBarrier correctly than the program below has never to be able to show any result.

It catches reordering of write operations every time I run it. It does not matter if I run it in Debug or Release. It also does not matter if I run it as 32bit or 64bit application.

Why does it happen?

    using System;
    using System.Threading;
    using System.Threading.Tasks;

    namespace FlipFlop
    {
        class Program
        {
            //Declaring these variables as volatile should instruct compiler to 
            //flush all caches from registers into the memory.
            static volatile int a;
            static volatile int b;

            //Track a number of iteration that it took to detect operation reordering.
            static long iterations = 0;

            static object locker = new object();

            //Indicates that operation reordering is not found yet.
            static volatile bool continueTrying = true;

            //Indicates that Check method should continue.
            static volatile bool continueChecking = true;

            static void Main(string[] args)
            {
                //Restarting test until able to catch reordering.
                while (continueTrying)
                {
                    iterations++;
                    var checker = new Task(Check);
                    var writter = new Task(Write);
                    lock (locker)
                    {
                        continueChecking = true;
                        checker.Start();

                    }
                    writter.Start();
                    checker.Wait();
                    writter.Wait();
                }
                Console.ReadKey();
            }

            static void Write()
            {
                //Writing is locked until Main will start Check() method.
                lock (locker)
                {
                    //Using memory barrier should prevent opration reordering.
                    a = 1;
                    Thread.MemoryBarrier();
                    b = 10;
                    Thread.MemoryBarrier();
                    b = 20;
                    Thread.MemoryBarrier();
                    a = 2;

                    //Stops spinning in the Check method.
                    continueChecking = false;
                }
            }

            static void Check()
            {
                //Spins until finds operation reordering or stopped by Write method.
                while (continueChecking)
                {
                    int tempA = a;
                    int tempB = b;

                    if (tempB == 10 && tempA == 2)
                    {
                        continueTrying = false;
                        Console.WriteLine("Caught when a = {0} and b = {1}", tempA, tempB);
                        Console.WriteLine("In " + iterations + " iterations.");
                        break;
                    }
                }
            }
        }
    }
Dennis
  • 2,615
  • 2
  • 19
  • 20
  • b/c this is their very idea. Memory barriers (write) just make sure all operation up to the moment are flushed, hence the following ones are ordered part the barrier. – bestsss May 28 '11 at 17:29
  • the most interesting thing in your code is that removing all the `Thread.MemoryBarrier();` lines fixes your problem =) – Mikant May 28 '11 at 17:55
  • 1
    @Mikant: No, that does _not_ fix the problem. It just makes it very very unlikely. Let it run for a few days and it still might happen. – H H May 28 '11 at 21:16

4 Answers4

10

You aren't cleaning the variables between tests, so (for all but the first) initially a is 2 and b is 20 - before Write has done anything.

Check can get that initial value of a (so tempA is 2), and then Write can get in, get as far as changing b to 10.

Now Check reads the b (so tempB is 10).

Et voila. No re-order necessary to repro.

Reset a and b to 0 between runs and I expect it will go away.

edit: confirmed; "as is" I get the issue almost immediately (<2000 iterations); but by adding:

while (continueTrying)
{
    a = b = 0; // reset <======= added this

it then loops for any amount of time without any issue.

Or as a flow:

Write                   A=  B=        Check

(except first run)      2   20
                                      int tempA = a;
a = 1;                  1   20
Thread.MemoryBarrier();
b = 10;                 1   10
                                      int tempB = b;
Marc Gravell
  • 1,026,079
  • 266
  • 2,566
  • 2,900
  • @Dennis no problem; I think he deleted then undeleted, so it wasn't there. Accepting his is the correct thing to do – Marc Gravell May 28 '11 at 21:35
3

I don't think this is re-ordering.

This piece of code is simply not thread-safe:

 while (continueChecking)
 {
     int tempA = a;
     int tempB = b;
     ...

I think this scenario is possible:

  1. int tempA = a; executes with the values of the last loop (a == 2)
  2. There is a context switch to the Write thread
  3. b = 10 and the loop stops
  4. There is a context switch to the Check thread
  5. int tempB = b; executes with b == 10

I notice that the calls to MemoryBarrier() enhance the chances of this scenario. Probably because they cause more context-switching.

H H
  • 263,252
  • 30
  • 330
  • 514
0

The result has nothing to do with reordering, with memory barries, or with volatile. All these constructs are needed to avoid effects of compiler or CPU reordering of the instructions.

But this program would produce the same result even assuming fully consistent single-CPU memory model and no compiler optimization.

First of all, notice that there will be multiple Write() tasks started in parallel. They are running sequentially due to lock() inside Write(), but a signle Check() method can read a and b produced by different instances of Write() tasks.

Because Check() function has no synchronization with Write function - it can read a and b at two arbitrary and different moments. There is nothing in your code that prevents Check() from reading a produced by previous Write() at one moment and then reading b produced by following Write() at another moment. First of all you need synchronization (lock) in Check() and then you might (but probably not in this case) need memory barriers and volatile to fight with memory model problems.

This is all you need:

        int tempA, tempB;
        lock (locker)
        {
            tempA = a;
            tempB = b;
        }
Michael Entin
  • 7,189
  • 3
  • 21
  • 26
  • 2
    It looks a bit more interesting than that. If there is no re-ordering, what scenario would gives those values for tempA/tempB? Note there is only one Write per test – Marc Gravell May 28 '11 at 17:40
  • (the lock is just meant to delay access so the Write doesn't happen too soon; as it happens, it doesn't necessarily do this, as there may be a delay between Start and the actual start - but it comes close enough, it seems) – Marc Gravell May 28 '11 at 17:43
  • @Marc - the Checker starts before Writer, so it has a chance to observe all the writes that Writer does. MemoryBarrier in Writer only makes things worse, since it increases the change for Checker to see all intermediate values – Michael Entin May 28 '11 at 17:45
  • 1
    The OP is not tryig to discuss mutex - it really is focusing on the intended re-ordering prevention. The "lock" suggestion at the end really misses what his is trying to illustrate, IMO. Again, the lock in the question ***is not intended*** to provide a mutex; it is just intended to delay Write until the reader has started. – Marc Gravell May 28 '11 at 17:54
  • @Marc - there are MANY Write() per test. Maybe that is the real cause of confusion :) – Michael Entin May 28 '11 at 18:25
  • @Michael - no, one Write, many read loops. By "test" I mean iterations++ – Marc Gravell May 28 '11 at 18:53
  • 1
    "that there will be multiple Write() tasks started in parallel" - this is very wrong. 1 Write / iteration. – H H May 28 '11 at 18:57
-1
  1. If you use MemoryBarrier in writer, why don't you do that in checker? Put Thread.MemoryBarrier(); before int tempA = a;.

  2. Calling Thread.MemoryBarrier(); so many times blocks all of the advantages of the method. Call it only once before or after a = 1;.

dtb
  • 213,145
  • 36
  • 401
  • 431
Mikant
  • 299
  • 3
  • 18
  • This doesn't really explain what's going on. How do these suggestions fix the problem in terms of the .NET memory model? – dtb May 28 '11 at 17:48
  • @dtb it was a bit clearer before you edited my post and deleted a line that my post could be a clue to @Dennis... there is nothing mysterious happening in his code. and there are no problems with .NET memory model. everything works as written. so i think Dennis is able to get an answer to the question following my .writings. – Mikant May 28 '11 at 18:01
  • 1
    Maybe Dennis is able to get the answer following your clues, but why don't you simply provide the answer directly for everyone to see? – dtb May 28 '11 at 18:09