C# 易失性 vs. 互锁 vs. 锁定
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/154551/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
Volatile vs. Interlocked vs. lock
提问by core
Let's say that a class has a public int counter
field that is accessed by multiple threads. This int
is only incremented or decremented.
假设一个类有一个public int counter
由多个线程访问的字段。这int
只会增加或减少。
To increment this field, which approach should be used, and why?
要增加这个字段,应该使用哪种方法,为什么?
lock(this.locker) this.counter++;
,Interlocked.Increment(ref this.counter);
,- Change the access modifier of
counter
topublic volatile
.
lock(this.locker) this.counter++;
,Interlocked.Increment(ref this.counter);
,- 将 的访问修饰符更改
counter
为public volatile
。
Now that I've discovered volatile
, I've been removing many lock
statements and the use of Interlocked
. But is there a reason not to do this?
现在我已经发现了volatile
,我已经删除了许多lock
语句和Interlocked
. 但有理由不这样做吗?
采纳答案by Orion Edwards
Worst (won't actually work)
最差(实际上不起作用)
Change the access modifier of
counter
topublic volatile
将访问修饰符更改
counter
为public volatile
As other people have mentioned, this on its own isn't actually safe at all. The point of volatile
is that multiple threads running on multiple CPUs can and will cache data and re-order instructions.
正如其他人所提到的,这本身实际上根本不安全。关键volatile
是在多个 CPU 上运行的多个线程可以并且将会缓存数据和重新排序指令。
If it is notvolatile
, and CPU A increments a value, then CPU B may not actually see that incremented value until some time later, which may cause problems.
如果不是volatile
,并且 CPU A 增加了一个值,那么 CPU B 可能直到一段时间后才真正看到该增加的值,这可能会导致问题。
If it is volatile
, this just ensures the two CPUs see the same data at the same time. It doesn't stop them at all from interleaving their reads and write operations which is the problem you are trying to avoid.
如果是volatile
,这只是确保两个 CPU 同时看到相同的数据。它根本不会阻止它们交错读取和写入操作,而这正是您试图避免的问题。
Second Best:
次好的:
lock(this.locker) this.counter++
;
lock(this.locker) this.counter++
;
This is safe to do (provided you remember to lock
everywhere else that you access this.counter
). It prevents any other threads from executing any other code which is guarded by locker
.
Using locks also, prevents the multi-CPU reordering problems as above, which is great.
这样做是安全的(前提是您记得lock
访问您访问的其他任何地方this.counter
)。它可以防止任何其他线程执行由locker
. 使用锁也可以防止上面的多 CPU 重新排序问题,这很好。
The problem is, locking is slow, and if you re-use the locker
in some other place which is not really related then you can end up blocking your other threads for no reason.
问题是,锁定很慢,如果你locker
在其他没有真正相关的地方重新使用它,那么你最终可能会无缘无故地阻塞你的其他线程。
Best
最好的事物
Interlocked.Increment(ref this.counter);
Interlocked.Increment(ref this.counter);
This is safe, as it effectively does the read, increment, and write in 'one hit' which can't be interrupted. Because of this, it won't affect any other code, and you don't need to remember to lock elsewhere either. It's also very fast (as MSDN says, on modern CPUs, this is often literally a single CPU instruction).
这是安全的,因为它可以在“一次命中”中有效地进行读取、增量和写入,并且不会被中断。因此,它不会影响任何其他代码,您也不需要记住在其他地方锁定。它也非常快(正如 MSDN 所说,在现代 CPU 上,这通常实际上是单个 CPU 指令)。
I'm not entirely sure however if it gets around other CPUs reordering things, or if you also need to combine volatile with the increment.
但是,我不完全确定它是否可以绕过其他 CPU 重新排序,或者您是否还需要将 volatile 与增量结合起来。
InterlockedNotes:
联锁注意事项:
- INTERLOCKED METHODS ARE CONCURRENTLY SAFE ON ANY NUMBER OF COREs OR CPUs.
- Interlocked methods apply a full fence around instructions they execute, so reordering does not happen.
- Interlocked methods do not need or even do not support access to a volatile field, as volatile is placed a half fence around operations on given field and interlocked is using the full fence.
- 互锁方法同时在任何数量的内核或 CPU 上都是安全的。
- 互锁方法在它们执行的指令周围应用一个完整的围栏,因此不会发生重新排序。
- 互锁方法不需要甚至不支持访问 volatile 字段,因为 volatile 在给定字段上的操作周围放置了半围栏,而互锁正在使用完整围栏。
Footnote: What volatile is actually good for.
脚注: volatile 实际上有什么好处。
As volatile
doesn't prevent these kinds of multithreading issues, what's it for? A good example is saying you have two threads, one which always writes to a variable (say queueLength
), and one which always reads from that same variable.
由于volatile
不能防止这些类型的多线程问题,它有什么用?一个很好的例子是说你有两个线程,一个总是写入一个变量(比如queueLength
),一个总是从同一个变量中读取。
If queueLength
is not volatile, thread A may write five times, but thread B may see those writes as being delayed (or even potentially in the wrong order).
如果queueLength
不是 volatile,线程 A 可能会写入五次,但线程 B 可能会看到这些写入被延迟(甚至可能以错误的顺序)。
A solution would be to lock, but you could also use volatile in this situation. This would ensure that thread B will always see the most up-to-date thing that thread A has written. Note however that this logic onlyworks if you have writers who never read, and readers who never write, andif the thing you're writing is an atomic value. As soon as you do a single read-modify-write, you need to go to Interlocked operations or use a Lock.
一种解决方案是锁定,但您也可以在这种情况下使用 volatile。这将确保线程 B 将始终看到线程 A 编写的最新内容。但是请注意,此逻辑仅适用于从不阅读的作者和从不写作的读者,并且您正在编写的内容是原子值。一旦你做了一个读-修改-写,你就需要去互锁操作或使用锁。
回答by Lou Franco
Interlocked functions do not lock. They are atomic, meaning that they can complete without the possibility of a context switch during increment. So there is no chance of deadlock or wait.
互锁功能不锁定。它们是原子的,这意味着它们可以在增量期间完成而无需上下文切换。所以没有机会出现死锁或等待。
I would say that you should always prefer it to a lock and increment.
我会说你应该总是更喜欢它而不是锁定和增量。
Volatile is useful if you need writes in one thread to be read in another, and if you want the optimizer to not reorder operations on a variable (because things are happening in another thread that the optimizer doesn't know about). It's an orthogonal choice to how you increment.
如果您需要在一个线程中写入以在另一个线程中读取,并且您希望优化器不对变量的操作重新排序(因为优化器不知道的事情正在另一个线程中发生),则 Volatile 很有用。这是您如何递增的正交选择。
This is a really good article if you want to read more about lock-free code, and the right way to approach writing it
如果您想阅读更多关于无锁代码以及编写它的正确方法,这是一篇非常好的文章
回答by Jon Skeet
EDIT:As noted in comments, these days I'm happy to use Interlocked
for the cases of a single variablewhere it's obviouslyokay. When it gets more complicated, I'll still revert to locking...
编辑:正如在评论中指出,这几天我很高兴地使用Interlocked
了的情况下,单变量的地方是明显没关系。当它变得更复杂时,我仍然会恢复锁定......
Using volatile
won't help when you need to increment - because the read and the write are separate instructions. Another thread could change the value after you've read but before you write back.
volatile
当您需要递增时,使用将无济于事 - 因为读取和写入是单独的指令。另一个线程可能会在您阅读之后但在您写回之前更改该值。
Personally I almost always just lock - it's easier to get right in a way which is obviouslyright than either volatility or Interlocked.Increment. As far as I'm concerned, lock-free multi-threading is for real threading experts, of which I'm not one. If Joe Duffy and his team build nice libraries which will parallelise things without as much locking as something I'd build, that's fabulous, and I'll use it in a heartbeat - but when I'm doing the threading myself, I try to keep it simple.
就我个人而言,我几乎总是锁定 - 以一种显然比波动性或 Interlocked.Increment正确的方式更容易获得正确的结果。就我而言,无锁多线程适用于真正的线程专家,我不是其中之一。如果 Joe Duffy 和他的团队构建了很好的库,可以并行化事物,而不会像我构建的那样多锁定,那就太棒了,我会在心跳中使用它 - 但是当我自己做线程时,我会尝试把事情简单化。
回答by Rob Walker
lock(...) works, but may block a thread, and could cause deadlock if other code is using the same locks in an incompatible way.
lock(...) 有效,但可能会阻塞线程,并且如果其他代码以不兼容的方式使用相同的锁,则可能导致死锁。
Interlocked.* is the correct way to do it ... much less overhead as modern CPUs support this as a primitive.
Interlocked.* 是正确的方法……因为现代 CPU 支持将其作为原语,所以开销要小得多。
volatile on its own is not correct. A thread attempting to retrieve and then write back a modified value could still conflict with another thread doing the same.
volatile 本身是不正确的。尝试检索并写回修改后的值的线程仍可能与执行相同操作的另一个线程发生冲突。
回答by Michael Damatov
"volatile
" does not replace Interlocked.Increment
! It just makes sure that the variable is not cached, but used directly.
” volatile
“不换Interlocked.Increment
!它只是确保变量没有被缓存,而是直接使用。
Incrementing a variable requires actually three operations:
增加一个变量实际上需要三个操作:
- read
- increment
- write
- 读
- 增量
- 写
Interlocked.Increment
performs all three parts as a single atomic operation.
Interlocked.Increment
将所有三个部分作为单个原子操作执行。
回答by Kenneth Xu
I did some test to see how the theory actually works: kennethxu.blogspot.com/2009/05/interlocked-vs-monitor-performance.html. My test was more focused on CompareExchnage but the result for Increment is similar. Interlocked is not necessary faster in multi-cpu environment. Here is the test result for Increment on a 2 years old 16 CPU server. Bare in mind that the test also involves the safe read after increase, which is typical in real world.
我做了一些测试,看看这个理论是如何工作的:kennethxu.blogspot.com/2009/05/interlocked-vs-monitor-performance.html。我的测试更侧重于 CompareExchnage,但 Increment 的结果相似。Interlocked 在多 CPU 环境中不需要更快。这是 Increment 在 2 年前的 16 CPU 服务器上的测试结果。请记住,该测试还涉及增加后的安全读取,这在现实世界中很常见。
D:\>InterlockVsMonitor.exe 16
Using 16 threads:
InterlockAtomic.RunIncrement (ns): 8355 Average, 8302 Minimal, 8409 Maxmial
MonitorVolatileAtomic.RunIncrement (ns): 7077 Average, 6843 Minimal, 7243 Maxmial
D:\>InterlockVsMonitor.exe 4
Using 4 threads:
InterlockAtomic.RunIncrement (ns): 4319 Average, 4319 Minimal, 4321 Maxmial
MonitorVolatileAtomic.RunIncrement (ns): 933 Average, 802 Minimal, 1018 Maxmial
回答by Zach Saw
Either lock or interlocked increment is what you are looking for.
您正在寻找锁定或互锁增量。
Volatile is definitely not what you're after - it simply tells the compiler to treat the variable as always changing even if the current code path allows the compiler to optimize a read from memory otherwise.
Volatile 绝对不是你想要的——它只是告诉编译器将变量视为总是在变化,即使当前的代码路径允许编译器优化从内存中读取的内容。
e.g.
例如
while (m_Var)
{ }
if m_Var is set to false in another thread but it's not declared as volatile, the compiler is free to make it an infinite loop (but doesn't mean it always will) by making it check against a CPU register (e.g. EAX because that was what m_Var was fetched into from the very beginning) instead of issuing another read to the memory location of m_Var (this may be cached - we don't know and don't care and that's the point of cache coherency of x86/x64). All the posts earlier by others who mentioned instruction reordering simply show they don't understand x86/x64 architectures. Volatile does notissue read/write barriers as implied by the earlier posts saying 'it prevents reordering'. In fact, thanks again to MESI protocol, we are guaranteed the result we read is always the same across CPUs regardless of whether the actual results have been retired to physical memory or simply reside in the local CPU's cache. I won't go too far into the details of this but rest assured that if this goes wrong, Intel/AMD would likely issue a processor recall! This also means that we do not have to care about out of order execution etc. Results are always guaranteed to retire in order - otherwise we are stuffed!
如果 m_Var 在另一个线程中设置为 false 但它没有声明为 volatile,编译器可以通过检查 CPU 寄存器(例如 EAX 因为那是m_Var 从一开始就被读取)而不是向 m_Var 的内存位置发出另一个读取(这可能被缓存 - 我们不知道也不关心,这就是 x86/x64 的缓存一致性点)。之前提到指令重新排序的其他人的所有帖子都表明他们不了解 x86/x64 架构。挥发性确实不发出读/写障碍,正如之前的帖子所暗示的那样,“它会阻止重新排序”。事实上,再次感谢 MESI 协议,我们保证我们读取的结果在 CPU 之间总是相同的,无论实际结果是已退休到物理内存还是仅仅驻留在本地 CPU 的缓存中。我不会深入讨论这个细节,但请放心,如果出现问题,英特尔/AMD 可能会召回处理器!这也意味着我们不必关心乱序执行等。结果总是保证按顺序退出 - 否则我们会被塞满!
With Interlocked Increment, the processor needs to go out, fetch the value from the address given, then increment and write it back -- all that while having exclusive ownership of the entire cache line (lock xadd) to make sure no other processors can modify its value.
使用互锁增量,处理器需要出去,从给定的地址中获取值,然后递增并将其写回——同时拥有整个缓存行的独占所有权(锁定 xadd)以确保没有其他处理器可以修改它的价值。
With volatile, you'll still end up with just 1 instruction (assuming the JIT is efficient as it should) - inc dword ptr [m_Var]. However, the processor (cpuA) doesn't ask for exclusive ownership of the cache line while doing all it did with the interlocked version. As you can imagine, this means other processors could write an updated value back to m_Var after it's been read by cpuA. So instead of now having incremented the value twice, you end up with just once.
使用 volatile,您最终仍将只有 1 条指令(假设 JIT 是有效的) - inc dword ptr [m_Var]。但是,处理器 (cpuA) 在对互锁版本执行所有操作时并不要求对缓存行拥有独占所有权。可以想象,这意味着其他处理器可以在 cpuA 读取后将更新的值写回 m_Var。因此,不是现在将值增加两次,而是最终只增加一次。
Hope this clears up the issue.
希望这能解决问题。
For more info, see 'Understand the Impact of Low-Lock Techniques in Multithreaded Apps' - http://msdn.microsoft.com/en-au/magazine/cc163715.aspx
有关详细信息,请参阅“了解多线程应用程序中低锁定技术的影响”- http://msdn.microsoft.com/en-au/magazine/cc163715.aspx
p.s. What prompted this very late reply? All the replies were so blatantly incorrect (especially the one marked as answer) in their explanation I just had to clear it up for anyone else reading this. shrugs
ps 是什么促使这么晚的回复?所有回复在他们的解释中都非常不正确(尤其是标记为答案的那个),我只需要为其他阅读此内容的人澄清一下。耸耸肩
p.p.s. I'm assuming that the target is x86/x64 and not IA64 (it has a different memory model). Note that Microsoft's ECMA specs is screwed up in that it specifies the weakest memory model instead of the strongest one (it's always better to specify against the strongest memory model so it is consistent across platforms - otherwise code that would run 24-7 on x86/x64 may not run at all on IA64 although Intel has implemented similarly strong memory model for IA64) - Microsoft admitted this themselves - http://blogs.msdn.com/b/cbrumme/archive/2003/05/17/51445.aspx.
pps 我假设目标是 x86/x64 而不是 IA64(它有不同的内存模型)。请注意,Microsoft 的 ECMA 规范被搞砸了,因为它指定了最弱的内存模型而不是最强的内存模型(最好针对最强的内存模型进行指定,以便跨平台保持一致 - 否则代码将在 x86 上运行 24-7/ x64 可能根本无法在 IA64 上运行,尽管英特尔已经为 IA64 实现了类似的强大内存模型) - 微软自己承认了这一点 - http://blogs.msdn.com/b/cbrumme/archive/2003/05/17/51445.aspx.
回答by zihotki
I second Jon Skeet's answer and want to add the following links for everyone who want to know more about "volatile" and Interlocked:
我支持 Jon Skeet 的回答,并希望为想要了解更多“volatile”和 Interlocked 的每个人添加以下链接:
原子性、波动性和不变性是不同的,第一部分 - (Eric Lippert 的 Fabulous Adventures In Coding)
Atomicity, volatility and immutability are different, part two
Atomicity, volatility and immutability are different, part three
Sayonara Volatile - (Wayback Machine snapshot of Joe Duffy's Weblog as it appeared in 2012)
Sayonara Volatile -(Joe Duffy 2012 年出现的博客的 Wayback Machine 快照)
回答by Vadim S.
I would like to add to mentioned in the other answers the difference between volatile
, Interlocked
, and lock
:
我想添加到其他的答案之间的区别提到的volatile
,Interlocked
和lock
:
The volatile keyword can be applied to fields of these types:
- Reference types.
- Pointer types (in an unsafe context). Note that although the pointer itself can be volatile, the object that it points to cannot. In other words, you cannot declare a "pointer" to be "volatile".
- Simple types such as
sbyte
,byte
,short
,ushort
,int
,uint
,char
,float
, andbool
. - An enum type with one of the following base types:
byte
,sbyte
,short
, ushort,int
, oruint
. - Generic type parameters known to be reference types.
IntPtr
andUIntPtr
.
- 参考类型。
- 指针类型(在不安全的上下文中)。请注意,尽管指针本身可以是 volatile,但它指向的对象不能。换句话说,您不能将“指针”声明为“易失性”。
- 简单的类型,如
sbyte
,byte
,short
,ushort
,int
,uint
,char
,float
,和bool
。 - 具有以下基本类型之一的枚举类型:
byte
、sbyte
、short
、 ushortint
、 或uint
。 - 已知为引用类型的泛型类型参数。
IntPtr
和UIntPtr
。
Other types, including double
and long
, cannot be marked "volatile"
because reads and writes to fields of those types cannot be guaranteed
to be atomic. To protect multi-threaded access to those types of
fields, use the Interlocked
class members or protect access using the
lock
statement.
其他类型,包括double
and long
,不能被标记为“volatile”,因为对这些类型的字段的读取和写入不能保证是原子的。要保护对这些类型字段的多线程访问,请使用Interlocked
类成员或使用lock
语句保护访问
。