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

C# 中的计时器初始化和争用条件

我认为这与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(上,故障模式的可能性更大。