简体   繁体   English

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

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

I've just written a method that is called by multiple threads simultaneously and I need to keep track of when all the threads have completed. 我刚刚编写了一个由多个线程同时调用的方法,我需要跟踪所有线程何时完成。 The code uses this pattern: 代码使用此模式:

private void RunReport()
{
   _reportsRunning++;

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

This is the only place within the code that _reportsRunning 's value is changed, and the method takes about a second to run. 这是代码中唯一的_reportsRunning值被更改的地方,该方法需要大约一秒钟才能运行。

Occasionally when I have more than six or so threads running reports together the final result for _reportsRunning can get down to -1. 偶尔当我有超过六个左右的线程一起运行报告时,_reportsRunning的最终结果可以降到-1。 If I wrap the calls to _runningReports++ and _runningReports-- in a lock then the behaviour appears to be correct and consistent. 如果我在锁中包含对_runningReports++_runningReports--的调用,那么该行为似乎是正确且一致的。

So, to the question: When I was learning multithreading in C++ I was taught that you didn't need to synchronize calls to increment and decrement operations because they were always one assembly instruction and therefore it was impossible for the thread to be switched out mid-call. 所以,问题是:当我在C ++中学习多线程时,我被教导你不需要同步调用递增和递减操作,因为它们总是一个汇编指令,因此线程不可能在中间切换-呼叫。 Was I taught correctly, and if so, how come that doesn't hold true for C#? 我是否正确地教过,如果是这样的话,那对C#来说怎么回事?

A ++ operator is not atomic in C# (and I doubt it is guaranteed to be atomic in C++) so yes, your counting is subject to race conditions. A ++运算符在C#中不是原子的(我怀疑它在C ++中保证是原子的)所以是的,你的计数受竞争条件的影响。

Use Interlocked.Increment and .Decrement 使用Interlocked.Increment和.Decrement

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

So, to the question: When I was learning multithreading in C++ I was taught that you didn't need to synchronize calls to increment and decrement operations because they were always one assembly instruction and therefore it was impossible for the thread to be switched out mid-call. 所以,问题是:当我在C ++中学习多线程时,我被教导你不需要同步调用递增和递减操作,因为它们总是一个汇编指令,因此线程不可能在中间切换-呼叫。 Was I taught correctly, and if so how come that doesn't hold true for C#? 我是否正确地教过,如果是这样的话怎么会对C#不适用?

This is incredibly wrong. 这是非常错误的。

On some architectures, like x86, there are single increment and decrement instructions. 在某些体系结构(如x86)上,有单个递增和递减指令。 Many architectures do not have them and need to do separate loads and stores. 许多架构没有它们,需要单独加载和存储。 Even on x86, there is no guarantee the compiler will generate the memory version of these instructions - it'll likely load into a register first, especially if it needs to do several operations with the result. 即使在x86上,也无法保证编译器会生成这些指令的内存版本 - 它可能首先加载到寄存器中,特别是如果它需要对结果执行多个操作。

Even if the compiler could be guaranteed to always generate the memory version of increment and decrement on x86, that still does not guarantee atomicity - two CPU's could modify the variable simultaneously and get inconsistent results. 即使编译器可以保证始终在x86上生成递增和递减的内存版本,但仍然不能保证原子性 - 两个CPU可以同时修改变量并获得不一致的结果。 The instruction would need the lock prefix to force it to be an atomic operation - compilers never emit the lock variant by default since it is less performant since it guarantees the action is atomic. 该指令需要使用锁定前缀来强制它成为原子操作 - 编译器默认情况下从不发出锁定变量,因为它的性能较差,因为它保证动作是原子的。

Consider the following x86 assembly instruction: 请考虑以下x86汇编指令:

inc [i]

If I is initially 0 and the code is run on two threads on two cores, the value after both threads finish could legally be either 1 or 2, since there is no guarantee that one thread will complete its read before the other thread finishes its write, or that one thread's write will even be visible before the other threads read. 如果我最初为0并且代码在两个核心上的两个线程上运行,则两个线程完成后的值可以合法地为1或2,因为无法保证一个线程将在另一个线程完成其写入之前完成其读取,或者在其他线程读取之前,一个线程的写入甚至可以看到。

Changing this to: 将此更改为:

lock inc [i]

Will result in getting a final value of 2. 将导致最终值为2。

Win32's InterlockedIncrement and InterlockedDecrement and .NET's Interlocked.Increment and Interlocked.Decrement result in doing the equivalent (possibly the exact same machine code) of lock inc . Win32的InterlockedIncrementInterlockedDecrement以及.NET的Interlocked.IncrementInterlocked.Decrement导致执行lock inc的等效(可能完全相同的机器代码)。

You were taught wrong. 你被教导错了。

There does exist hardware with atomic integer increment, so it's possible that what you were taught was right for the hardware and compiler you were using at the time. 确实存在具有原子整数增量的硬件,因此您所教授的内容可能适用于您当时使用的硬件和编译器。 But in general in C++ you can't even guarantee that incrementing a non-volatile variable writes memory consecutively with reading it, let alone atomically with reading. 但一般来说,在C ++中,你甚至不能保证递增非易失性变量会在读取内存时连续写入内存,更不用说读取原子。

Incrementing the int is one instruction but what about loading the value in the register? 增加int是一条指令,但是如何在寄存器中加载值呢?

That's what i++ effectively does: 这就是i++有效地做的事情:

  1. load i into a register i加载到寄存器中
    • increment the register 递增寄存器
    • unload the register into i 将寄存器卸载到i中

As you can see there are 3 (this may be different on other platforms) instructions which in any stage the cpu can context switch into a different thread leaving your variable in an unknown state. 正如您所看到的,有3个(在其他平台上可能会有所不同)指令,在任何阶段,cpu都可以将上下文切换到不同的线程,使您的变量处于未知状态。

You should use Interlocked.Increment and Interlocked.Decrement to solve that. 您应该使用Interlocked.IncrementInterlocked.Decrement来解决这个问题。

No, you need to synchronize access. 不,您需要同步访问权限。 On Windows you can do this easily with InterlockedIncrement() and InterlockedDecrement(). 在Windows上,您可以使用InterlockedIncrement()和InterlockedDecrement()轻松完成此操作。 I'm sure there are equivalents for other platforms. 我确信其他平台也有等价物。

EDIT: Just noticed the C# tag. 编辑:刚刚注意到C#标签。 Do what the other guy said. 做其他人说的话。 See also: I've heard i++ isn't thread safe, is ++i thread-safe? 另请参阅: 我听说i ++不是线程安全的,++ i是线程安全的吗?

Any kind of increment/decrement operation in a higher level language (and yes, even C is higher level compared to machine instructions) is not atomic by nature. 更高级语言中的任何类型的递增/递减操作(是的,甚至C与机器指令相比更高级别)本质上不是原子的。 However, each processor platform usually has primitives that support various atomic operations . 但是,每个处理器平台通常都具有支持各种原子操作的基元

If your lecturer was referring to machine instructions, Increment and Decrement operations are likely to be atomic. 如果您的讲师指的是机器指令,则递增和递减操作可能是原子的。 Yet, that is not always correct on the ever increasing multi-core platforms of today, unless they guarantee coherency . 然而,在当今不断增加的多核平台上,这并不总是正确的,除非它们保证一致性

The higher level languages usually implement support for atomic transactions using low level atomic machine instructions. 更高级别的语言通常使用低级原子机器指令实现对原子transactions支持 This is provided as the interlock mechanism by the higher level API. 这是由更高级API提供的互锁机制。

x++ probably isn't atomic, but ++x might be (not sure offhand, but if you consider the difference between post- and pre-increment it should be clear why pre- is more amenable to atomicity). x ++可能不是原子的,但++ x可能是(不确定的,但如果考虑后增量和预增量之间的差异,应该清楚为什么pre-更适合原子性)。

A bigger point is, if these runs take a second to run each, the amount of time added by a lock is going to be noise compared to the runtime of the method itself. 更重要的一点是,如果这些运行花费一秒钟来运行每个运行,那么锁定所添加的时间量将与方法​​本身的运行时间相比是噪声。 It's probably not worth monkeying with trying to remove the lock in this case - you've got a correct solution with locking, that will likely not have a visible difference in performance from the non-locking solution. 在这种情况下尝试移除锁定可能不值得一试 - 你有一个正确的锁定解决方案,这可能与非锁定解决方案的性能没有明显差异。

On a single-processor 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-时操作只是原子操作位编译器)。 Also, movsb/movsw/movsl are atomic ways of moving a byte/word/longword; 此外,movsb / movsw / movsl是移动字节/字/长字的原子方式; a compiler isn't apt to use those as the normal way of assigning variables, but one could have an atomic-move utility function. 编译器不喜欢将它们用作分配变量的常规方法,但可以使用原子移动实用程序函数。 It would be possible for a virtual memory manager to be written in such a way that those instructions would behave atomically if a page fault occurs on the write, but I don't think that's normally guaranteed. 虚拟内存管理器有可能以这样的方式编写,即如果在写入时发生页面错误,那些指令将以原子方式运行,但我认为通常不会保证。

On a multi-processor machine, all bets are off unless one uses explicit interlocked instructions (invokable via special library calls). 在多处理器机器上,除非使用显式互锁指令(通过特殊库调用可调用),否则所有投注都将关闭。 The most versatile instruction which is commonly available is CompareExchange. 通用的最通用的指令是CompareExchange。 That instruction will alter a memory location only if it contains an expected value; 该指令只有在包含预期值时才会改变内存位置; it will return the value it had when it decided whether or not to alter it. 当它决定是否改变它时,它将返回它所具有的值。 If one wishes to "xor" a variable with 1, one could do something like (in vb.net) 如果有人希望用1“变量”变量,可以做一些像(在vb.net中)

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

This approach allows one to perform any sort of atomic update to a variable whose new value should depend on the old value. 这种方法允许对新变量应该依赖于旧值的变量执行任何种类的原子更新。 For certain common operations like increment and decrement, there are faster alternatives, but the CompareExchange allows one to implement other useful patterns as well. 对于某些常见操作(如递增和递减),有更快的替代方法,但CompareExchange也允许实现其他有用的模式。

Important caveats: (1) Keep the loop as short as possible; 重要提示:(1)保持环路尽可能短; the longer the loop, the more likely it is that another task will hit the variable during the loop, and the more time will be wasted each time that happens; 循环时间越长,另一个任务在循环期间命中变量的可能性就越大,每次发生时浪费的时间就越多; (2) a specified number of updates, divided arbitrarily among threads, will always complete, since the only way a thread can forced to re-execute the loop is if some other thread has made useful progress; (2)在线程之间任意划分的指定数量的更新将始终完成,因为线程可以强制重新执行循环的唯一方法是,如果某个其他线程已经取得了有用的进展; if some threads can perform updates without making forward progress toward completion, however, the code may become live-locked. 但是,如果某些线程可以在不向前完成的情况下执行更新,则代码可能会变为实时锁定。

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

相关问题 我是否需要使用默认的 TaskScheduler 同步任务之间的资源访问? - Do I need to synchronize resource access between Tasks with the default TaskScheduler? 在同时调用方法时是否需要同步访问方法参数和本地? - Do I need to synchronize access the method arguments and locals when calling a method concurrently? 如何从另一个线程访问 WinForms 控件,即与 GUI 线程同步? - How to access a WinForms control from another thread i.e. synchronize with the GUI thread? 如何同步访问数据库,一次一个线程 - How to synchronize access to a database, one thread at a time 我应该*始终*同步访问从多个线程使用的所有双字段/属性/变量? - should I *always* synchronize access to all double field/property/variables that used from more than one thread? 为什么需要UI线程检查 - Why do I need a UI Thread check 我需要清除Thread.CurrentPrincipal吗? - Do I need to clear Thread.CurrentPrincipal? 如何从另一个线程同步UI和访问对象? - How to synchronize UI and access objects from another thread? 如果我调用Thread.Join(),我是否需要volatile? - Do I need volatile if I call Thread.Join()? 访问CustomTextParagraphProperties我需要参考什么? - What do I need to reference to access CustomTextParagraphProperties?
 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM