在计时器回调中递增 Int32 是否线程安全
本文关键字:Int32 是否 线程 安全 计时器 回调 | 更新日期: 2023-09-27 18:33:59
我想递增一个整数,该整数在计时器事件处理程序中递增并由主线程和其他工作线程(即一个编写器线程和多个读取器线程(读取。它会是线程安全的吗?
我的应用程序中有一个每 5 秒运行一次的计时器:
MyClock = new System.Threading.Timer(
new TimerCallback(this.Ticker), null, Timeout.Infinite, Timeout.Infinite );
我像这样打开:
MyClock.Change(5000, 5000);
如果我像这样在Ticker
处理程序中增加一个整数:
tickerCounter++;
然后,是否可以从同一应用程序的主线程或工作线程执行只读访问?它会是线程安全的吗?读取器是否有可能读取部分值或导致线程异常?
首先:阅读 Eric Lippert 的博客文章:你所说的"线程安全"是什么?它将改变您对线程安全的看法,以及您将来提出类似问题的方式。
读取将是原子的 - 您永远不会看到"一半"更新 - 但您不一定会看到最新值。(当然,这意味着下一个增量可能会看到一个"旧"(较低(值来增加。
此外,增量本身并不安全 - 如果有多个线程同时递增,它们都可以读取相同的值,然后在本地递增,然后写入新值 - 因此结果可能是增量 1,即使 10 个线程递增。
应考虑使用 Interlocked.Increment
来执行增量。如果你也使变量易变,我相信当你只想阅读它时,直接阅读它应该是安全的。
也许您可以使用Interlocked.Increment(ref Int32)
进行递增?这是作为原子操作进行的。
像这样递增
tickerCounter++;
在多线程中,没有锁定,就不是线程安全的。可以使用 Interlocked
类执行无锁、线程安全的递增。
如果只有一个线程在修改该值,并且许多线程读取该值,则tickerCounter++
是线程安全的,因为任何线程都不会遭受部分读取。当然还是有比赛条件的,但即使你用Interlocked
,也会有比赛。
System.Threading.Timer
组件调用ThreadPool
上的回调。没有针对重叠调用的保护。如果回调时间超过period
,则两个后续调用可能同时在不同的ThreadPool
线程上运行。在这种情况下,可能会丢失增量,换句话说,递增的Int32
值可能会小于调用总数。不存在线程异常或撕裂值的风险,但存在应用程序的正确性受到损害的风险,这可以说更糟。
实际上,如果计时器每 5 秒滴答一次,并且回调只增加整数值,则重叠的概率为零。若要获得非零重叠概率,必须在处理程序内执行繁重的工作,或者将period
设置为非常小的值,或两者兼而有之。危险区域的时间段低于 ~50 毫秒,因为这是操作系统可以挂起线程的最长持续时间(根据我的实验,演示(。当然,依靠"安全期"估计并不是编写健壮软件的方法。建议您使用 lock
语句或 Interlocked
API 正确同步整数的增量。从 .NET 6 开始,您还可以选择切换到 PeriodicTimer
组件,这可以通过使用方式自然地防止重叠调用(示例(。