Initialization of a timer and race condition in C #?

I saw this code in Richter's book:

The following code demonstrates how to invoke the thread pool thread method starting immediately and then every 2 seconds after that:

/*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 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 which lines #7,#8 mean

And, of course, why it was initialized (line number 9) to Timeout.Infinite (which, obviously: "do not start the timer")

(I understand the general goal of preventing duplication, but I believe that there is a condition for the GC race here).

Edit

System.Threading namespace

+7
source share
2 answers

I think this is not related to the GC, but to avoid the race condition:

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

So here is the scenario:

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

  • the current thread is unloaded before the assignment ends => s_timer is still null

  • the timer wakes up in another thread and calls Status , but the original thread has not yet completed the assignment operation !

  • Status refers to s_timer , which is a null reference => BOOM!

With his method, this cannot happen, for example. with the same scenario:

  • a timer is created but not started

  • current thread unloaded

  • nothing happens because the timer has not yet started creating events

  • initial thread started again

  • it completes the assignment => s_timer refers to a timer

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

+11
source

This is a race, but there is more than it seems at first glance. The obvious failure mode is when the main thread loses the processor and does not work for some time, more than a second. And thus, it never ends up updating the s_timer, kaboom variable in the callback.

On machines with multiple processor cores, there is a much more subtle problem. In this case, the updated value of the variable should really be visible on the processor core, which runs the callback code. What reads memory through the cache, this cache may contain outdated content and still has the s_time variable at zero when reading. This usually requires a memory barrier. Its low-level version is available from the Thread.MemoryBarrier () method. There is no code in the published version of the code to guarantee that this will happen.

This works in practice because the memory barrier is implicit. The operating system cannot start the threadpool thread required here to receive a callback without having to use a memory barrier. A side effect now also ensures that the callback thread uses the update value of the s_time variable. Based on this side effect, it does not win any prizes, but works in practice. But it will also not work if Richter's workaround is not used, since the barrier may well be accepted before the appointment. And, therefore, a more likely failure mode on processors with a weak memory model, such as Itanium and ARM.

+3
source

All Articles