我在里希特的书上看到了这段代码:
以下代码演示了如何进行线程池线程调用 一个方法立即开始,然后每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
答案 0 :(得分:11)
我认为这与GC没有关系,而是为了避免竞争条件:
赋值操作不是原子操作:首先创建Timer对象然后分配它。
所以这是一个场景:
new Timer(...)
创建计时器并开始“计数”
当前主题在分配结束前被抢占 => s_timer
仍然为空
计时器唤醒另一个线程并调用Status
,但初始线程尚未完成分配操作!
Status
访问s_timer
,这是空引用 =>的 BOOM!强>
用他的方法不可能发生,例如使用相同的方案:
计时器已创建,但未启动
当前主题被抢先
没有任何反应因为计时器尚未开始举起活动
初始线程再次运行
结束作业 => s_timer
引用计时器
计时器安全启动:以后对Status
的任何通话都有效,因为s_timer
是有效参考
答案 1 :(得分:3)
这是一场比赛,但除了眼睛之外还有更多的东西。显而易见的故障模式是主线程丢失处理器并且运行一段时间,超过一秒钟。因此,永远不会在回调中更新s_timer变量kaboom。
在具有多个处理器核心的计算机上存在更微妙的问题。因为更新的变量值实际上需要在运行回调代码的cpu核心上可见。它通过缓存读取内存,该缓存容易包含陈旧内容,并且在读取时仍然将s_time变量设置为null。这通常需要内存屏障。 Thread.MemoryBarrier()方法提供了它的低级版本。发布的版本中没有任何代码可以确保发生这种情况。
它在实践中有效,因为内存屏障是隐式的。操作系统无法启动线程池线程,此处需要获取回调,而不会自行占用内存障碍。其副作用现在还确保回调线程使用s_time变量的更新值。依靠这种副作用不会赢得任何奖品,但在实践中有效。但如果不使用Richter的解决方法也不会起作用,因为在分配之前很可能会采取屏障。因此,对于具有弱内存模型的处理器,如Itanium和ARM,可能会出现故障模式。