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