7

我在 Richter 的书上看到了这段代码:

以下代码演示了如何让线程池线程立即开始调用方法,然后每 2 秒调用一次:

/*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*/    }

但是,(对不起),我还是不明白行是什么#7,#8意思

当然 - 为什么将其初始化(第 9 行)为Timeout.Infinite(显然是:“不要启动计时器”)

(我确实了解防止重叠的一般目的,但我相信这里还有一个GC竞争条件 pov。)

编辑

命名空间是System.Threading

4

2 回答 2

11

我认为这与 GC 无关,而是为了避免竞争条件

赋值操作不是原子的:首先创建 Timer 对象,然后对其进行赋值。

所以这是一个场景:

  • new Timer(...)创建计时器并开始“计数”

  • 当前线程在分配结束之前被抢占=>s_timer仍然为空

  • 计时器在另一个线程上唤醒并调用Status,但初始线程尚未完成分配操作

  • Status访问s_timer它是一个空引用=> BOOM!

使用他的方法是不可能发生的,例如在相同的情况下:

  • 计时器已创建但未启动

  • 当前线程被抢占

  • 没有任何反应,因为计时器尚未开始引发事件

  • 初始线程再次运行

  • 结束分配=>s_timer 引用计时器

  • 计时器安全启动:任何未来的调用Status都是有效的,因为s_timer它是一个有效的参考

于 2013-06-23T17:30:10.987 回答
3

这是一场比赛,但它的意义远不止眼前所见。明显的故障模式是主线程失去处理器并且有一段时间没有运行,超过一秒钟。因此永远不会在回调中更新 s_timer 变量 kaboom。

在具有多个处理器内核的机器上存在一个更微妙的问题。因为更新的变量值实际上需要在运行回调代码的 cpu 核心上可见。它通过缓存读取内存,该缓存很可能包含陈旧的内容,并且在读取时仍然有 s_time 变量为空。这通常需要一个内存屏障。Thread.MemoryBarrier() 方法提供了它的低级版本。发布的版本中没有任何代码可以确保发生这种情况。

它在实践中有效,因为内存屏障是隐式的。操作系统无法启动线程池线程,此处需要让回调继续进行,而本身没有内存屏障。其副作用现在还确保回调线程使用 s_time 变量的更新值。依靠这种副作用不会赢得任何奖品,但可以在实践中发挥作用。但如果不使用 Richter 的解决方法也将不起作用,因为在分配之前很可能会采取障碍。因此,在内存模型较弱的处理器(如安腾和 ARM)上,更可能出现故障模式。

于 2013-06-23T18:45:56.750 回答