휘발성 vs. 연동 vs. 잠금


671

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

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

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

내가 발견 한 지금 volatile, 나는 많은 lock진술과의 사용을 제거했습니다 Interlocked. 그러나 이것을하지 않는 이유가 있습니까?


C # 참조 의 스레딩을 읽으십시오 . 그것은 당신의 질문에 대한 내용을 다룹니다. 세 가지 각각 다른 목적과 부작용이 있습니다.
spoulson

1
simple-talk.com/blogs/2012/01/24/… 당신은 배열에서 volitable의 사용을 볼 수 있습니다.
eran otzap

50
이것은 "스프링클러 시스템이 절대로 활성화되지 않았다는 것을 알게되었으므로이를 제거하고 연기 경보로 교체 할 것"이라고 말합니다. 이 작업을 수행하지 않는 이유 는 매우 위험 하고 거의 혜택을주지 않기 때문입니다 . 코드를 변경하는 데 시간이 걸리면 멀티 스레드를 줄이십시오 . 멀티 스레드 코드를 더 위험하고 쉽게 깨뜨리는 방법을 찾지 마십시오!
Eric Lippert

1
우리 집에는 스프링클러 화재 경보기가 있습니다. 한 스레드에서 카운터를 증가시키고 다른 스레드에서 카운터를 읽을 때 lock (또는 Interlocked) 키워드 volatile 키워드 가 모두 필요한 것 같습니다 . 진실?
yoyo

2
@yoyo 아니요, 둘 다 필요하지 않습니다.
David Schwartz

답변:


859

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

의 액세스 한정자 변경 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가 작성한 최신 정보를 항상 볼 수 있습니다. 참고 그러나이 논리는 단지 당신이 쓰는 결코 읽어 본 적이 작가와 독자가 있다면, 작동 것은 당신이있는 거 쓰기가 원자 값 인 경우. 단일 읽기-수정-쓰기를 수행하자마자 연동 작업으로 이동하거나 잠금을 사용해야합니다.


29
"내가 확실치 않다 ... 휘발성과 증분을 결합해야한다면." 우리는 심판에 의해 휘발성을 전달할 수 없으므로 AFAIK를 결합 할 수 없습니다. 그건 그렇고 좋은 대답입니다.
Hosam Aly

41
고마워요! "실제 휘발성이 무엇인가"에 대한 귀하의 주석은 내가 찾고 휘발성을 어떻게 사용하고 싶은지를 확인한 것입니다.
Jacques Bosch

7
즉, var가 volatile로 선언 된 경우 컴파일러는 코드가 나타날 때마다 var의 값이 동일 (즉, 휘발성) 상태로 유지되지 않는다고 가정합니다. 따라서 while (m_Var) {} 및 다른 스레드에서 m_Var가 false로 설정된 루프에서 컴파일러는 m_Var의 값으로 이미로드 된 레지스터에 이미 무엇이 있는지 확인하지 않고 m_Var에서 값을 읽습니다. 다시. 그러나 휘발성을 선언하지 않으면 루프가 무한대로 계속된다는 것을 의미하지는 않습니다. 휘발성을 지정하면 다른 스레드에서 m_Var가 false로 설정된 경우에만 루프가 발생하지 않습니다.
Zach 톱

35
@Zach Saw : C ++의 메모리 모델에서 휘발성은이를 설명하는 방법입니다 (기본적으로 장치 매핑 메모리에 유용하지만 그다지 중요하지는 않습니다). CLR 의 메모리 모델 (이 질문에는 C #로 태그 지정됨)에서 휘발성은 해당 스토리지 위치에 대한 읽기 및 쓰기 주변에 메모리 장벽을 삽입한다는 것입니다. 메모리 장벽 (및 일부 조립 지침의 특수한 고정 변형)은 프로세서 가 물건을 재정렬하지 말라고 알려주며 상당히 중요합니다 ...
Orion Edwards

19
@ZachSaw : C #의 휘발성 필드는 C # 컴파일러와 jit 컴파일러가 값을 캐시하는 특정 최적화를 수행하지 못하게합니다. 또한 여러 스레드에서 읽기 및 쓰기 순서를 확인할 수 있습니다. 구현 세부 사항으로서 읽기 및 쓰기에 메모리 장벽을 도입하여 그렇게 할 수 있습니다. 보장 된 정확한 의미는 사양에 설명되어 있습니다. 이 스펙은 모든 휘발성 쓰기 및 읽기 의 일관된 순서 가 모든 스레드에 의해 준수됨을 보증 하지는 않습니다 .
Eric Lippert

147

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

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

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


16
지금부터 잠금없는 코딩을 잊어 버린 +1.
Xaqron

5
잠금이없는 코드는 (FSB) 버스 또는 CPU 간 수준에서 어떤 단계에서 잠금을 수행 할 때 실제로 잠금이없는 것은 아닙니다. 그러나 이러한 낮은 수준에서의 잠금은 일반적으로 잠금이 발생하는 대역폭을 포화시키지 않는 한 더 빠릅니다.
Zach는

2
Interlocked에는 아무런 문제가 없습니다. 그것은 정확히 당신이 찾고 전체 잠금 ()보다 빠릅니다.
Jaap

5
@Jaap은 : 네, 요즘은 나는 것이 진정한 하나의 카운터 연동 사용합니다. 변수에 대한 여러 잠금없는 업데이트 사이의 상호 작용을 해결하려고 애 쓰고 싶지 않습니다 .
Jon Skeet

6
@ZachSaw : 두 번째 의견에서는 연동 된 작업이 어떤 단계에서 "잠금"된다고 말합니다. "잠금"이라는 용어는 일반적으로 하나의 작업이 무한한 시간 동안 자원의 배타적 제어를 유지할 수 있음을 의미합니다. 잠금없는 프로그래밍의 주요 장점은 소유 작업이 방해를 받아 결과적으로 리소스를 사용할 수 없게되는 위험을 피할 수 있다는 것입니다. 인터 로킹 된 클래스가 사용하는 버스 동기화는 "일반적으로 더 빠르지"않습니다. 대부분의 시스템에서는 제한 시간이 가장 제한적인 반면 잠금은 그렇지 않습니다.
supercat

44

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

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

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

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


4
다른 방법으로 말하면, 연동 된 변경은 완전히 보호되며 원자 적입니다. 휘발성 부재는 부분적으로 만 고정되므로 스레드 안전을 보장하지 않습니다.
JoeGeeky

1
실제로 변수가 캐시되지 않았는지 확인 volatile하지 마십시오 . 캐시하는 방법에 제한이 있습니다. 예를 들어, CPU의 L2 캐시는 하드웨어에서 일관성있게 만들어지기 때문에 여전히 캐시에 저장 될 수 있습니다. 여전히 prefected 수 있습니다. 쓰기는 여전히 캐시 등에 게시 될 수 있습니다. (나는 자크가에서지고 있었는지라고 생각한다.)
데이비드 슈워츠

42

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

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

예 :

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 .


3
흥미 롭군 이것을 참조 할 수 있습니까? 나는 이것을 행복하게 투표하지만, 내가 읽은 리소스와 일치하는 높은 투표 응답 후 3 년 동안 적극적인 언어로 게시하려면 좀 더 확실한 증거가 필요합니다.
Steven Evers

참조하려는 부분을 지적 할 수 있다면 어딘가에서 물건을 파헤 쳐서 기쁠 것입니다 (x86 / x64 공급 업체 영업 비밀을 제공하지 않았기 때문에 Wiki, Intel에서 쉽게 사용할 수 있어야합니다. PRM (프로그래머 참조 매뉴얼), MSFT 블로그, MSDN 또는 이와 유사한 것)
Zach Saw

2
누구나 CPU 캐싱을 방지하려는 이유는 무엇입니까? 캐시 일관성을 수행하기위한 전용 부동산 (크기 및 비용면에서 무시할 수없는)은 그럴 경우 완전히 낭비됩니다 ... 그래픽 카드, PCI 장치 등과 같은 캐시 일관성이 필요하지 않은 경우에는 설정하지 않습니다. 연속 기입 캐시 라인.
Zach는

4
네, 당신이 말하는 모든 것은 100 %가 아니라면 99 % 이상입니다. 이 사이트는 당신이 직장에서 개발을 서두르고있을 때 (대부분) 매우 유용하지만 불행히도 (게임) 투표에 해당하는 답변의 정확성은 없습니다. 따라서 기본적으로 스택 오버플로에서는 독자가 실제로 이해하는 것이 아니라 독자에 대한 대중적인 이해가 무엇인지 느낄 수 있습니다. 때로는 최고의 대답은 순수한 횡설수설입니다. 불행히도 이것은 문제를 해결하는 동안 읽은 사람들에게 번식하는 것입니다. 이해할 수는 있지만 아무도 모든 것을 알 수는 없습니다.
user1416420

1
@ BenVoigt .NET이 실행되는 모든 아키텍처에 대해 계속 대답 할 수는 있지만 몇 페이지가 걸리므로 SO에는 적합하지 않습니다. 임의의 것보다 가장 널리 사용되는 .NET 기반 하드웨어 mem 모델을 기반으로 사람들을 교육하는 것이 훨씬 좋습니다. 그리고 '어디서나'라는 의견으로 사람들이 캐시를 플러시 / 무효화한다고 가정하는 실수를 수정하고있었습니다. 그들은 어떤 하드웨어를 지정하지 않고 기본 하드웨어에 대해 가정했습니다.
Zach

16

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

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

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

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

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


11

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

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

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


8

이론이 실제로 어떻게 작동하는지 확인하기 위해 몇 가지 테스트를 수행했습니다 : 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

테스트 한 코드 샘플은 사소한 것이지만 실제로 그렇게 테스트하는 것은 의미가 없습니다! 최선의 방법은 다양한 방법이 실제로 수행하는 작업을 이해하고 사용 시나리오에 따라 적절한 방법을 사용하는 것입니다.
Zach는

@Zach, 스레드 안전 방식으로 카운터를 늘리는 시나리오에 대한 논의는 여기에서 이루어졌습니다. 다른 사용 시나리오는 무엇입니까? 또는 어떻게 테스트 하시겠습니까? 의견 BTW에 감사드립니다.
Kenneth Xu

요점은 인공 테스트입니다. 실제 시나리오에서 종종 같은 위치를 망치지 않을 것입니다. 그렇다면 서버 상자에 표시된 것처럼 FSB에 의해 병목 현상이 발생하는 것입니다. 어쨌든, 귀하의 블로그에서 내 답변을보십시오.
Zach는

2
다시 찾아 봐 실제 병목 현상이 FSB와 관련된 경우 모니터 구현에서 동일한 병목 현상이 발생해야합니다. 실제 차이점은 Interlocked가 바쁜 대기 및 재 시도를 수행하여 고성능 계산에서 실제 문제가된다는 것입니다. 적어도 내 의견이 Interlocked가 항상 계산에 올바른 선택은 아니라는 관심을 끌기를 바랍니다. 사람들이 대안을 찾고 있다는 사실이 그것을 잘 설명했습니다. 긴 가산기 gee.cs.oswego.edu/dl/jsr166/dist/jsr166edocs/jsr166e/…
Kenneth Xu

8

3

나는 차이 사이의 다른 답변에서 언급에 추가 할 volatile, Interlocked그리고 lock:

volatile 키워드는 다음 유형의 필드에 적용 할 수 있습니다 .

  • 참조 유형.
  • 포인터 유형 (안전하지 않은 컨텍스트) 포인터 자체는 휘발성 일 수 있지만, 가리키는 객체는 불가능합니다. 즉, "포인터"를 "휘발성"으로 선언 할 수 없습니다.
  • 같은 간단한 유형 sbyte, byte, short, ushort, int, uint, char, float,와 bool.
  • 다음과 같은 기본 유형 중 하나 열거 형 : byte, sbyte, short, USHORT, int또는 uint.
  • 참조 유형으로 알려진 일반 유형 매개 변수.
  • IntPtr그리고 UIntPtr.

다른 유형 을 포함 double하고는 long, 읽고 그 유형의 필드 쓰기가 원자 보장 할 수 없기 때문에 "휘발성"표시 할 수 없습니다. 이러한 유형의 필드에 대한 다중 스레드 액세스를 보호하려면 Interlocked클래스 멤버를 사용하거나 lock명령문을 사용하여 액세스를 보호하십시오 .

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.