繁体   English   中英

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

[英]Do I need to synchronize thread access to an int?

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

private void RunReport()
{
   _reportsRunning++;

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

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

偶尔当我有超过六个左右的线程一起运行报告时,_reportsRunning的最终结果可以降到-1。 如果我在锁中包含对_runningReports++_runningReports--的调用,那么该行为似乎是正确且一致的。

所以,问题是:当我在C ++中学习多线程时,我被教导你不需要同步调用递增和递减操作,因为它们总是一个汇编指令,因此线程不可能在中间切换-呼叫。 我是否正确地教过,如果是这样的话,那对C#来说怎么回事?

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

使用Interlocked.Increment和.Decrement

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

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

这是非常错误的。

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

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

请考虑以下x86汇编指令:

inc [i]

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

将此更改为:

lock inc [i]

将导致最终值为2。

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

你被教导错了。

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

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

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

  1. i加载到寄存器中
    • 递增寄存器
    • 将寄存器卸载到i中

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

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

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

编辑:刚刚注意到C#标签。 做其他人说的话。 另请参阅: 我听说i ++不是线程安全的,++ i是线程安全的吗?

更高级语言中的任何类型的递增/递减操作(是的,甚至C与机器指令相比更高级别)本质上不是原子的。 但是,每个处理器平台通常都具有支持各种原子操作的基元

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

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

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

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

machine, if one isn't using virtual memory, x++ (rvalue ignored) is likely to translate into a single atomic INC instruction on x86 architectures (if x is long, the operation is only atomic when using a 32-bit compiler). 机器上,如果没有使用虚拟内存,x ++(rvalue ignored)可能会转换为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)在线程之间任意划分的指定数量的更新将始终完成,因为线程可以强制重新执行循环的唯一方法是,如果某个其他线程已经取得了有用的进展; 但是,如果某些线程可以在不向前完成的情况下执行更新,则代码可能会变为实时锁定。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM