developer tip

휘발성 vs. 연동 vs. 잠금

optionbox 2020. 10. 2. 22:03
반응형

휘발성 vs. 연동 vs. 잠금


클래스 public int counter에 여러 스레드가 액세스 하는 필드 가 있다고 가정 해 보겠습니다 . 이것은 int증가 또는 감소 만합니다.

이 필드를 늘리려면 어떤 접근 방식을 사용해야하며 그 이유는 무엇입니까?

  • lock(this.locker) this.counter++;,
  • Interlocked.Increment(ref this.counter);,
  • 의 액세스 한정자 변경 counter에를 public volatile.

이제 발견 volatile했으므로 많은 lock문과 Interlocked. 그러나 이것을하지 않을 이유가 있습니까?


최악 (실제로 작동하지 않음)

의 액세스 한정자 변경 counter에를public volatile

다른 사람들이 언급했듯이 이것은 그 자체로는 실제로 전혀 안전하지 않습니다. 요점은 volatile여러 CPU에서 실행되는 여러 스레드가 데이터를 캐시하고 명령을 다시 정렬 할 수 있다는 것입니다.

그렇지 않은 경우 volatileCPU A가 값을 증가 시키면 CPU B는 나중에 얼마 동안 증가 된 값을 실제로 보지 못하여 문제가 발생할 수 있습니다.

인 경우 volatile두 CPU가 동시에 동일한 데이터를 볼 수 있도록합니다. 피하려는 문제인 읽기 및 쓰기 작업을 인터리빙하는 것을 전혀 막지 못합니다.

두번째로 좋은:

lock(this.locker) this.counter++;

이것은 안전하게 할 수 있습니다 ( lock당신이 접근하는 다른 모든 곳 을 기억 한다면 this.counter). 다른 스레드가에서 보호하는 다른 코드를 실행하는 것을 방지합니다 locker. 잠금을 사용하면 위와 같이 멀티 CPU 재정렬 문제를 방지 할 수 있습니다.

문제는 잠금이 느리고 locker실제로 관련이없는 다른 장소에서 를 재사용 하면 이유없이 다른 스레드를 차단할 수 있다는 것입니다.

베스트

Interlocked.Increment(ref this.counter);

이는 중단 할 수없는 '한 번의 적중'으로 읽기, 증가 및 쓰기를 효과적으로 수행하므로 안전합니다. 이 때문에 다른 코드에 영향을주지 않으며 다른 곳에서도 잠그는 것을 기억할 필요가 없습니다. 또한 매우 빠릅니다 (MSDN이 말했듯이 최신 CPU에서는 말 그대로 단일 CPU 명령입니다).

그러나 다른 CPU가 물건을 재정렬하거나 휘발성과 증분을 결합 해야하는지 완전히 확실하지 않습니다.

연동 참고 :

  1. 연동 된 방법은 모든 코어 또는 CPU에서 동시에 안전합니다.
  2. 인터록 메서드는 실행하는 명령어 주위에 전체 펜스를 적용하므로 재정렬이 발생하지 않습니다.
  3. Interlocked 메서드 는 휘발성 필드에 대한 액세스를 필요로하지 않거나 지원하지도 않습니다. 휘발성은 주어진 필드의 작업 주위에 절반 울타리를 배치하고 interlocked는 전체 울타리를 사용하기 때문입니다.

각주 : 휘발성이 실제로 좋은 것.

으로 volatile그것을 무엇을, 멀티 스레딩 이러한 종류의 문제를 방지하지 않는 이유는 무엇입니까? 좋은 예는 두 개의 스레드가 있다는 것입니다. 하나는 항상 변수 (예 :)에 쓰고 queueLength다른 하나는 항상 동일한 변수에서 읽습니다.

queueLength휘발성이 아닌 경우 스레드 A는 5 번 쓸 수 있지만 스레드 B는 이러한 쓰기가 지연된 것으로 볼 수 있습니다 (또는 잠재적으로 잘못된 순서).

해결책은 잠그는 것이지만이 상황에서 휘발성을 사용할 수도 있습니다. 이렇게하면 스레드 B가 항상 스레드 A가 작성한 최신 내용을 볼 수 있습니다. 참고 그러나이 논리는 단지 당신이 쓰는 결코 읽어 본 적이 작가와 독자가 있다면, 작동 것은 당신이있는 거 쓰기가 원자 값 인 경우. 단일 읽기-수정-쓰기를 수행하는 즉시 연동 작업으로 이동하거나 잠금을 사용해야합니다.


편집 : 주석에서 언급했듯이 요즘 분명히 괜찮은 단일 변수Interlocked 의 경우에 사용하게 되어 기쁩니다 . 더 복잡해지면 여전히 잠금 상태로 되돌릴 것입니다.

volatile읽기와 쓰기가 별도의 명령이기 때문에 증분해야 할 때 사용 하면 도움이되지 않습니다. 읽은 후 다시 쓰기 전에 다른 스레드에서 값을 변경할 수 있습니다.

개인적으로 저는 거의 항상 잠그고 있습니다. 변동성 또는 연동보다 분명히 올바른 방식으로 바로 잡는 것이 더 쉽습니다 . 제가 아는 한, 잠금없는 멀티 스레딩은 실제 스레딩 전문가를위한 것입니다. Joe Duffy와 그의 팀이 내가 빌드 한 것만 큼 많은 잠금없이 일을 병렬화 할 멋진 라이브러리를 빌드한다면 그것은 훌륭하고 심장 박동으로 그것을 사용할 것입니다.하지만 내가 스레딩을 할 때 나는 간단하게 유지하십시오.


" volatile"는 대체하지 않습니다 Interlocked.Increment! 변수가 캐시되지 않고 직접 사용되는지 확인합니다.

변수를 늘리려면 실제로 세 가지 작업이 필요합니다.

  1. 읽다
  2. 증가
  3. 쓰다

Interlocked.Increment 세 부분을 모두 단일 원자 연산으로 수행합니다.


잠금 또는 연동 증분 중 하나를 찾고 있습니다.

Volatile은 확실히 당신이 추구하는 것이 아닙니다. 현재 코드 경로가 컴파일러가 메모리에서 읽기를 최적화하도록 허용하더라도 컴파일러에게 변수를 항상 변경되는 것으로 처리하도록 지시합니다.

예 :

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 not issue 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!

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.

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.

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

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

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.


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.

This is a really good article if you want to read more about lock-free code, and the right way to approach writing it

http://www.ddj.com/hpc-high-performance-computing/210604448


lock(...) works, but may block a thread, and could cause deadlock if other code is using the same locks in an incompatible way.

Interlocked.* is the correct way to do it ... much less overhead as modern CPUs support this as a primitive.

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.


I second Jon Skeet's answer and want to add the following links for everyone who want to know more about "volatile" and Interlocked:

Atomicity, volatility and immutability are different, part one - (Eric Lippert's 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)


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.

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

I would like to add to mentioned in the other answers the difference between volatile, Interlocked, and 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, and bool.
  • An enum type with one of the following base types: byte, sbyte, short, ushort, int, or uint.
  • Generic type parameters known to be reference types.
  • IntPtr and 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.

참고URL : https://stackoverflow.com/questions/154551/volatile-vs-interlocked-vs-lock

반응형