我需要将线程访问同步到 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 的 InterlockedIncrement
和 InterlockedDecrement
和 .NET 的 Interlocked.Increment
和 Interlocked.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 吗?的主要内容,如果未能解决你的问题,请参考以下文章