Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Timer initialization and Race condition in c#?

I saw this code on Richter's book :

The following code demonstrates how to have a thread pool thread call a method starting immediately and then every 2 seconds thereafter:

/*1*/    internal static class TimerDemo
/*2*/    {
/*3*/        private static Timer s_timer;
/*4*/        public static void Main()
/*5*/        {
/*6*/            Console.WriteLine("Checking status every 2 seconds");
/*7*/            // Create the Timer ensuring that it never fires. This ensures that
/*8*/            // s_timer refers to it BEFORE Status is invoked by a thread pool thread
/*9*/            s_timer = new Timer(Status, null, Timeout.Infinite, Timeout.Infinite);
/*10*/            // Now that s_timer is assigned to, we can let the timer fire knowing
/*11*/            // that calling Change in Status will not throw a NullReferenceException
/*12*/            s_timer.Change(0, Timeout.Infinite);
/*13*/            Console.ReadLine(); // Prevent the process from terminating
/*14*/        }
/*15*/        // This method's signature must match the TimerCallback delegate
/*16*/        private static void Status(Object state)
/*17*/        {
/*18*/            // This method is executed by a thread pool thread
/*20*/            Console.WriteLine("In Status at {0}", DateTime.Now);
/*21*/            Thread.Sleep(1000); // Simulates other work (1 second)
/*22*/            // Just before returning, have the Timer fire again in 2 seconds
/*23*/            s_timer.Change(2000, Timeout.Infinite);
/*24*/            // When this method returns, the thread goes back
/*25*/            // to the pool and waits for another work item
/*26*/        }
/*27*/    }

However , (sorry) , I still don't understand what lines #7,#8 means

And of course - why was it initialized (line #9) to Timeout.Infinite ( which is obviously : "don't start the timer")

( I do understand the general purpose for preventing overlaps , but I believe there is also a GC race condition pov here.)

edit

the namespace is System.Threading

like image 831
Royi Namir Avatar asked Jun 23 '13 17:06

Royi Namir


2 Answers

I think it's not related to the GC but rather to avoid a race condition:

The assignment operation is not atomic: first you create the Timer object then you assign it.

So here is a scenario:

  • new Timer(...) creates the timer and it starts "counting"

  • the current thread is preempted BEFORE the assignment ends => s_timer is still null

  • the timer wakes up on another thread and calls Status but the initial thread has not yet finished the assignment operation!

  • Status accesses s_timer which is a null reference => BOOM!

With his method it can't happen, e.g. with the same scenario:

  • the timer is created but does not start

  • the current thread is preempted

  • nothing happens because the timer has not yet started to raise events

  • the initial thread is running again

  • it ends the assignment => s_timer references the timer

  • the timer is started safely: any future call to Status is valid because s_timer is a valid reference

like image 109
Pragmateek Avatar answered Nov 14 '22 23:11

Pragmateek


It is a race, but there's more to it than meets the eye. The obvious failure mode is when the main thread loses the processor and doesn't run for a while, more than a second. And thus never gets around to updating the s_timer variable, kaboom in the callback.

A much more subtle issue is present on machines with multiple processor cores. In that the updated variable value actually needs to be visible on the cpu core that runs the callback code. Which reads memory through a cache, that cache is liable to contain stale content and still have the s_time variable at null when it is read. That normally requires a memory barrier. A low-level version of it is available from the Thread.MemoryBarrier() method. There is no code whatsoever in the posted version that ensures that this happens.

It works in practice because the memory barrier is implicit. The operating system cannot get a threadpool thread started, required here to get the callback going, without itself taking a memory barrier. The side effect of which now also also ensures that the callback thread uses the update value of the s_time variable. Relying on this side-effect doesn't win any prizes, but works in practice. But also won't work if Richter's workaround isn't used since the barrier may well be taken before the assignment. And thus the likelier failure mode on processors with a weak memory model, like Itanium and ARM.

like image 40
Hans Passant Avatar answered Nov 14 '22 23:11

Hans Passant