我是否需要同步对int的线程访问?

时间:2009-07-06 15:57:32

标签: c# c++ multithreading locking

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

private void RunReport()
{
   _reportsRunning++;

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

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

有时,当我有超过六个左右的线程一起运行报告时,_reportsRunning的最终结果可以降到-1。如果我将锁定中的_runningReports++_runningReports--包裹起来,则行为似乎是正确且一致的。

所以,对于这个问题:当我在C ++中学习多线程时,我被教导你不需要同步调用来增加和减少操作,因为它们总是一个汇编指令,因此线程不可能中途通话。我是否正确地教过,如果是这样的话,怎么会对C#不适用?

8 个答案:

答案 0 :(得分:27)

++运算符在C#中不是原子的(我怀疑它在C ++中保证是原子的)所以是的,你的计数受竞争条件的限制。

使用Interlocked.Increment和.Decrement

System.Threading.Interlocked.Increment(ref _reportsRunning);
try 
{
  ...
}
finally
{
   System.Threading.Interlocked.Decrement(ref _reportsRunning);
}

答案 1 :(得分:20)

  

所以,问题是:我什么时候   用C ++学习多线程我是   教你不需要   同步调用以增加和   减少操作因为它们是   总是一个汇编指令和   因此,这是不可能的   线程在呼叫中切换出来。   我是否正确教过,如果是这样的话   那对C#来说不合适吗?

这是非常错误的。

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

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

考虑以下x86汇编指令:

inc [i]

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

将此更改为:

lock inc [i]

将导致最终值为2.

Win32的InterlockedIncrementInterlockedDecrement以及.NET的Interlocked.IncrementInterlocked.Decrement导致执行lock inc的等效(可能是完全相同的机器代码)。< / p>

答案 2 :(得分:4)

你被教导错了。

确实存在具有原子整数增量的硬件,因此您所教授的内容可能适用于您当时使用的硬件和编译器。但是一般来说,在C ++中,你甚至不能保证增量非易失性变量会在读取内存时连续写入内存,更不用说读取原子。

答案 3 :(得分:3)

递增int是一条指令,但是如何在寄存器中加载值?

这就是i++有效地做的事情:

  1. i 加载到寄存器中
    • 递增寄存器
    • 将注册表卸载到 i
  2. 正如您所看到的,有3个(在其他平台上可能会有所不同)指令,在任何阶段,cpu都可以将上下文切换到不同的线程,使您的变量处于未知状态。

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

答案 4 :(得分:2)

不,您需要同步访问权限。在Windows上,您可以使用InterlockedIncrement()和InterlockedDecrement()轻松完成此操作。我确信其他平台也有等价物。

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

答案 5 :(得分:1)

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

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

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

答案 6 :(得分:0)

x ++可能不是原子的,但++ x可能是(不确定随意,但如果你考虑后增量和预增量之间的区别,应该清楚为什么pre-更适合原子性)。

更重要的一点是,如果这些运行花费一秒钟来运行每个运行,那么锁定添加的时间量将与方法​​本身的运行时间相比是噪声。在这种情况下尝试移除锁定可能不值得一试 - 你有一个正确的锁定解决方案,这可能与非锁定解决方案的性能没有明显区别。

答案 7 :(得分:0)

单处理器机器上,如果没有使用虚拟内存,x ++(忽略rvalue)可能会转换为x86体系结构上的单个原子INC指令(如果x很长,使用32位编译器时,该操作只是原子操作)。此外,movsb / movsw / movsl是移动字节/字/长字的原子方式;编译器不喜欢将它们用作分配变量的常规方法,但可以使用原子移动实用程序函数。如果在写入时发生页面错误,那么虚拟内存管理器可能会以这样的方式编写,即这些指令会原子地运行,但我认为这通常不会得到保证。

在多处理器计算机上,除非使用显式互锁指令(通过特殊库调用可调用),否则所有投注都将关闭。通用的最通用的指令是CompareExchange。该指令只有在包含预期值时才会改变内存位置;当它决定是否改变它时,它将返回它所具有的值。如果有人希望用1“变量”变量,那么可以做一些像(在vb.net中)

的变量
  Dim OldValue as Integer
  Do
    OldValue = Variable
  While Threading.Interlocked.CompareExchange(Variable, OldValue Xor 1, OldValue)  OldValue

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

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