Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why volatile and MemoryBarrier do not prevent operations reordering?

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;
                    }
                }
            }
        }
    }
like image 383
Dennis Avatar asked May 28 '11 16:05

Dennis


2 Answers

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;
like image 91
Marc Gravell Avatar answered Oct 27 '22 01:10

Marc Gravell


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.

like image 29
Henk Holterman Avatar answered Oct 26 '22 23:10

Henk Holterman