我需要将线程访问同步到 int 吗?

Posted

技术标签:

【中文标题】我需要将线程访问同步到 int 吗?【英文标题】:Do I need to synchronize thread access to an int? 【发布时间】:2009-07-06 15:57:31 【问题描述】:

我刚刚编写了一个由多个线程同时调用的方法,我需要跟踪所有线程何时完成。代码使用这种模式:

private void RunReport()

   _reportsRunning++;

   try
   
       //code to run the report
   
   finally
   
       _reportsRunning--;
   

这是代码中唯一更改_reportsRunning 值的地方,该方法运行大约需要一秒钟。

有时,当我有超过六个左右的线程一起运行报告时,_reportsRunning 的最终结果可能会降至 -1。如果我将对_runningReports++_runningReports-- 的调用封装在一个锁中,那么行为似乎是正确且一致的。

所以,对于这个问题:当我在 C++ 中学习多线程时,我被告知您不需要同步对递增和递减操作的调用,因为它们始终是一条汇编指令,因此线程不可能是在通话中切换。我的教学是否正确,如果正确,为什么这不适用于 C#?

【问题讨论】:

可能是一条指令来做一个值的递增。但是谁说编译器没有在方法执行期间优化掉内存位置,现在每个线程都在增加自己的副本在寄存器中,然后在后面的阶段写回。 【参考方案1】:

++ 运算符在 C# 中不是原子的(我怀疑它在 C++ 中肯定是原子的)所以是的,您的计数受竞争条件的影响。

使用 Interlocked.Increment 和 .Decrement

System.Threading.Interlocked.Increment(ref _reportsRunning);
try 

  ...

finally

   System.Threading.Interlocked.Decrement(ref _reportsRunning);

【讨论】:

感谢您的快速响应。我以前从未见过这些方法——非常有帮助。知道我的讲师是否正确地告诉我这是 C++ 中的原子操作? ++ 在 C++ 中也不是原子的。 如果您的讲师指的是机器增量和减量指令,那是正确的。 higher 级别的语言无法保证这一点。 C++ 没有指定任何原子操作。 Linux 使用 atomic_t 来支持原子整数操作。默认情况下它不是原子的。【参考方案2】:

所以,对于这个问题:当我 我在 C++ 中学习多线程 教导你不需要 同步调用增量和 减量操作,因为它们是 总是一个汇编指令和 因此这是不可能的 在通话中切换线程。 我教得是否正确,如果是,如何 来吧,这不适用于 C#?

这是非常错误的。

在某些体系结构上,例如 x86,有单个递增和递减指令。许多架构没有它们,需要进行单独的加载和存储。即使在 x86 上,也不能保证编译器会生成这些指令的内存版本 - 它可能会首先加载到寄存器中,尤其是当它需要对结果执行多个操作时。

即使可以保证编译器始终在 x86 上生成递增和递减的内存版本,但这仍然不能保证原子性 - 两个 CPU 可能同时修改变量并得到不一致的结果。该指令需要 lock 前缀来强制它成为原子操作 - 编译器默认不会发出 lock 变体,因为它保证操作是原子的,因此性能较低。

考虑以下 x86 汇编指令:

inc [i]

如果 I 最初为 0 并且代码在两个内核上的两个线程上运行,则两个线程完成后的值可以合法地为 1 或 2,因为不能保证一个线程将在另一个线程之前完成其读取完成其写入,或者一个线程的写入甚至会在其他线程读取之前可见。

将其更改为:

lock inc [i]

将导致最终值为 2。

Win32 的 InterlockedIncrementInterlockedDecrement 和 .NET 的 Interlocked.IncrementInterlocked.Decrement 导致执行与 lock inc 等效(可能完全相同的机器代码)。

【讨论】:

【参考方案3】:

你被教错了。

确实存在具有原子整数增量的硬件,因此您所学的内容可能适合您当时使用的硬件和编译器。但一般而言,在 C++ 中,您甚至无法保证递增非易失性变量会在读取内存时连续写入内存,更不用说读取时原子性地写入内存了。

【讨论】:

【参考方案4】:

增加int 是一条指令,但是如何将值加载到寄存器中呢?

这就是i++ 的实际作用:

    i 加载到寄存器中 增加寄存器 将寄存器卸载到i

如您所见,有 3 条(在其他平台上可能不同)指令,在任何阶段 cpu 都可以上下文切换到不同的线程,从而使您的变量处于未知状态。

您应该使用Interlocked.Increment 和Interlocked.Decrement 来解决这个问题。

【讨论】:

x86 确实具有增量和减量的内存版本,它们在一条指令中完成所有 3 个步骤。但它们不是原子的 - 如果其他 CPU 也在修改值,则无法保证其他 CPU 会看到正确的结果。【参考方案5】:

不,您需要同步访问。在 Windows 上,您可以使用 InterlockedIncrement() 和 InterlockedDecrement() 轻松完成此操作。我相信其他平台也有类似的。

编辑:刚刚注意到 C# 标签。照别人说的做。另见:I've heard i++ isn't thread safe, is ++i thread-safe?

【讨论】:

谢谢,这回答了我问题的另一半,看来我被教错了(或更可能被误解了)。很遗憾我不能同时接受这两个答案...抱歉。 @Martin:别担心!我不是为了荣耀和分数而来的。只是想帮忙。【参考方案6】:

高级语言中的任何类型的递增/递减操作(是的,即使C 与机器指令相比也是更高级别的)本质上不是原子的。但是,每个处理器平台通常都有primitives that support various atomic operations。

如果您的讲师指的是机器指令,则增量和减量操作可能是原子操作。然而,在当今不断增长的多核平台上,这并不总是正确的,除非他们保证coherency。

高级语言通常使用低级原子机器指令实现support for atomic transactions。这是由更高级别的 API 作为互锁机制提供的。

【讨论】:

【参考方案7】:

x++ 可能不是原子性的,但 ++x 可能是(不确定临时,但如果您考虑后增量和前增量之间的差异,应该清楚为什么 pre- 更适合原子性)。

更重要的一点是,如果这些运行每次运行都需要一秒钟,那么与方法本身的运行时间相比,锁添加的时间量将是噪音。在这种情况下,尝试移除锁可能不值得一笑了之——您有一个正确的锁定解决方案,与非锁定解决方案的性能可能没有明显差异。

【讨论】:

【参考方案8】:

单处理器机器上,如果不使用虚拟内存,x++(忽略右值)可能会转换为 x86 架构上的单个原子 INC 指令(如果 x 很长,该操作仅在使用 32 位编译器时是原子的)。此外, movsb/movsw/movsl 是移动字节/字/长字的原子方式;编译器不倾向于将它们用作分配变量的正常方式,但可以具有原子移动实用程序功能。如果写入时发生页面错误,虚拟内存管理器的编写方式可能会以原子方式运行,但我认为这通常不能保证。

在多处理器机器上,除非使用明确的互锁指令(可通过特殊库调用调用),否则所有赌注都将失败。最常用的指令是 CompareExchange。只有当它包含预期值时,该指令才会更改内存位置;当它决定是否改变它时,它将返回它所拥有的值。如果希望用 1 “异或”一个变量,可以执行类似(在 vb.net 中)

将 OldValue 调暗为整数 做 旧值 = 变量 While Threading.Interlocked.CompareExchange(Variable, OldValue Xor 1, OldValue) OldValue

这种方法允许对新值依赖于旧值的变量执行任何类型的原子更新。对于某些常见的操作,如递增和递减,有更快的替代方案,但 CompareExchange 也允许实现其他有用的模式。

重要注意事项: (1) 使循环尽可能短;循环越长,循环期间另一个任务就越有可能命中变量,每次发生的时候浪费的时间就越多; (2) 指定数量的更新,在线程之间任意划分,总是会完成,因为一个线程可以强制重新执行循环的唯一方法是,如果某个其他线程取得了有用的进展;但是,如果某些线程可以执行更新而不向前推进完成,则代码可能会变成活锁。

【讨论】:

以上是关于我需要将线程访问同步到 int 吗?的主要内容,如果未能解决你的问题,请参考以下文章

我是不是需要同步对仅由一个线程修改的列表的访问?

如何正确同步这两个线程?

线程同步

C# 中对同步集合的非锁定访问行为

将 Swift 调用同步到基于 C 的线程不安全库

多个线程可以在不同的地方访问一个向量吗?