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.)
the namespace is System.Threading
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
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With