[C#] 휘발성 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에서 실행되는 여러 스레드가 데이터를 캐시하고 명령을 다시 정렬 할 수 있다는 것입니다.

이 값이 아니고 volatile CPU 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. 휘발성은 주어진 필드에서 작업 주위에 절반 펜스를 배치하고 인터록은 전체 펜스를 사용하므로 인터록 된 메소드 는 휘발성 필드에 대한 액세스를 지원하지 않아도 됩니다.

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

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

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

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


답변

편집 : 의견에서 언급했듯이 요즘 Interlocked에는 단일 변수의 경우에는 분명히 괜찮습니다. 더 복잡해지면 여전히 잠금 상태로 돌아갑니다.

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

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


답변

volatile“는 대체되지 않습니다 Interlocked.Increment! 변수가 캐시되지 않고 직접 사용되도록합니다.

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

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

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


답변

당신이 찾고있는 것은 잠금 또는 연동 증분입니다.

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

예 :

while (m_Var)
{ }

다른 스레드에서 m_Var가 false로 설정되었지만 휘발성으로 선언되지 않은 경우 컴파일러는 CPU 레지스터 (예 : EAX)를 검사하여 무한 루프 (언제나 그렇다는 의미는 아님)로 만들 수 있습니다. m_Var의 메모리 위치에 대한 또 다른 읽기를 발행하는 대신 m_Var가 처음부터 페치 된 것 (캐시 될 수 있음-우리는 알지 못하고 신경 쓰지 않으며 x86 / x64의 캐시 일관성 지점) 이전에 명령어 순서를 언급 한 다른 사람들의 모든 게시물은 x86 / x64 아키텍처를 이해하지 못한다는 것을 보여줍니다. 휘발성이 수행 되지‘재주문 방지’라는 이전 게시물에서 암시 된대로 읽기 / 쓰기 장벽을 발행하십시오. 실제로 MESI 프로토콜 덕분에 실제 결과가 실제 메모리로 폐기되었는지 또는 단순히 로컬 CPU 캐시에 있는지 여부에 관계없이 읽은 결과가 CPU 전체에서 항상 동일하다는 것을 보장합니다. 자세한 내용은 다루지 않겠지 만, 이것이 잘못되면 인텔 / AMD가 프로세서 리콜을 발행 할 것입니다. 이것은 또한 우리가 고장난 실행 등을 신경 쓸 필요가 없다는 것을 의미합니다. 결과는 항상 순서대로 은퇴하도록 보장됩니다. 그렇지 않으면 우리는 채워집니다!

Interlocked Increment를 사용하면 프로세서가 나가고 주어진 주소에서 값을 가져온 다음 증가시키고 다시 기록해야합니다. 다른 모든 프로세서는 수정할 수 없도록 전체 캐시 라인 (lock xadd)의 독점 소유권을 갖습니다. 그 가치.

volatile을 사용하면 여전히 하나의 명령으로 끝납니다 (JIT가 효율적이라고 가정)-inc dword ptr [m_Var]. 그러나 프로세서 (cpuA)는 연동 버전으로 수행 한 모든 작업을 수행하는 동안 캐시 라인의 독점 소유권을 요구하지 않습니다. 아시다시피, 이는 다른 프로세서가 cpuA가 읽은 후 업데이트 된 값을 m_Var에 다시 쓸 수 있음을 의미합니다. 따라서 이제 값을 두 번 증가시키는 대신 한 번만 끝내십시오.

이것이 문제를 해결하기를 바랍니다.

자세한 내용은 ‘멀티 스레드 앱에서 로우 락 기술의 영향 이해’-http://msdn.microsoft.com/en-au/magazine/cc163715.aspx를 참조하십시오 .

ps 무엇이 이렇게 늦게 답장을 받았습니까? 모든 답글이 설명에 너무 끔찍하게 잘못되었습니다 (특히 답변으로 표시된 답변). 어깨를 으 s하다

pps 대상이 IA64가 아닌 x86 / x64라고 가정합니다 (다른 메모리 모델이 있음). Microsoft의 ECMA 사양은 가장 강력한 메모리 모델 대신 가장 약한 메모리 모델을 지정한다는 점에서 망설입니다. (가장 강력한 메모리 모델에 대해 지정하는 것이 플랫폼 전체에서 일관되게 지정하는 것이 좋습니다. 그렇지 않으면 x86 / 24에서 24-7로 실행되는 코드) 인텔은 IA64에 대해 유사한 강력한 메모리 모델을 구현했지만 x64는 IA64에서 전혀 실행되지 않을 수 있습니다)-Microsoft는이를 스스로 인정했습니다-http: //blogs.msdn.com/b/cbrumme/archive/2003/05/17/51445.aspx .


답변

연동 기능은 잠기지 않습니다. 그것들은 원자 적이므로 증분 동안 컨텍스트 전환 가능성없이 완료 할 수 있습니다. 따라서 교착 상태 나 대기의 가능성이 없습니다.

나는 항상 잠금 및 증가보다 선호해야한다고 말하고 싶습니다.

휘발성은 한 쓰레드의 쓰기가 다른 쓰레드에서 읽히고, 옵티마이 저가 변수에 대한 연산의 순서를 바꾸지 않기를 원하는 경우에 유용합니다. 증분 방법에 대한 직교 선택입니다.

잠금없는 코드에 대한 자세한 내용과 코드 작성 방법에 대한 자세한 내용은이 기사를 참조하십시오.

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


답변

lock (…)은 작동하지만 스레드를 차단할 수 있으며 다른 코드가 호환되지 않는 방식으로 동일한 잠금을 사용하는 경우 교착 상태가 발생할 수 있습니다.

Interlocked. *는 올바른 방법입니다. 최신 CPU가이를 기본으로 지원하므로 오버 헤드가 훨씬 줄어 듭니다.

휘발성 자체가 올바르지 않습니다. 수정 된 값을 검색 한 후 다시 쓰려고하는 스레드는 여전히 동일한 스레드를 수행하는 다른 스레드와 충돌 할 수 있습니다.


답변

이론이 실제로 어떻게 작동하는지 확인하기 위해 몇 가지 테스트를 수행했습니다 : kennethxu.blogspot.com/2009/05/interlocked-vs-monitor-performance.html . 내 테스트는 CompareExchnage에 중점을 두었지만 증가 결과는 비슷합니다. 멀티 CPU 환경에서는 인터 로킹이 더 빨리 필요하지 않습니다. 다음은 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