.NET에서 이중 체크 잠금에서 휘발성 수정 자 필요


85

여러 텍스트는 .NET에서 이중 검사 잠금을 구현할 때 잠그고있는 필드에 휘발성 수정자를 적용해야한다고 말합니다. 하지만 정확히 왜? 다음 예를 고려하십시오.

public sealed class Singleton
{
   private static volatile Singleton instance;
   private static object syncRoot = new Object();

   private Singleton() {}

   public static Singleton Instance
   {
      get 
      {
         if (instance == null) 
         {
            lock (syncRoot) 
            {
               if (instance == null) 
                  instance = new Singleton();
            }
         }

         return instance;
      }
   }
}

"잠금 (syncRoot)"이 필요한 메모리 일관성을 달성하지 못하는 이유는 무엇입니까? "잠금"문 이후 읽기와 쓰기가 모두 휘발성이므로 필요한 일관성이 달성된다는 것이 사실이 아닙니까?


2
이것은 이미 여러 번 씹어졌습니다. yoda.arachsys.com/csharp/singleton.html
Hans Passant

1
불행히도 그 기사에서 Jon은 '휘발성'을 두 번 언급했으며 두 참조 모두 그가 준 코드 예제를 직접 언급하지 않았습니다.
Dan Esparza 2011

우려 사항을 이해하려면이 기사를 참조하십시오. igoro.com/archive/volatile-keyword-in-c-memory-model-explained 기본적으로 JIT가 인스턴스 변수에 CPU 레지스터를 사용하는 것이 이론적으로 가능합니다. 특히 필요한 경우 거기에 약간의 추가 코드. 따라서 if 문을 두 번 수행하면 다른 스레드에서 변경 되더라도 동일한 값을 반환 할 수 있습니다. 실제로 대답은 조금 복잡한 잠금 문이 또는 (계속) 더 나은 여기에 물건을 만들기위한 책임을지지 않을 수있다
user2685937

(이전 주석 계속 참조)-여기에 실제로 일어나고 있다고 생각합니다.-기본적으로 변수를 읽거나 설정하는 것보다 복잡한 작업을 수행하는 코드는 JIT가 말하도록 트리거 할 수 있습니다. 최적화하는 것을 잊고로드하고 메모리에 저장합니다. 함수가 호출되면 JIT는 매번 메모리에서 직접 쓰고 읽는 대신 매번 레지스터를 저장 한 경우 레지스터를 저장하고 다시로드해야 할 수 있습니다. 자물쇠가 특별한 것이 아님을 어떻게 알 수 있습니까? 내가 이고르에서 이전의 코멘트에 게시 된 링크에서 봐 (다음 설명을 계속)
user2685937

(위의 2 개의 주석 참조)-Igor의 코드를 테스트했으며 새 스레드를 만들 때 그 주위에 잠금을 추가하고 루프를 만들었습니다. 인스턴스 변수가 루프 밖으로 들어 왔기 때문에 코드가 종료되지는 않습니다. while 루프에 간단한 로컬 변수 세트를 추가하면 여전히 루프에서 변수를 끌어 올릴 수 있습니다. 이제 if 문이나 메서드 호출 또는 yes와 같은 더 복잡한 것은 최적화를 방해하여 작동하게합니다. 따라서 복잡한 코드는 종종 JIT가 최적화하도록 허용하지 않고 직접 변수 액세스를 강제합니다. (다음 댓글 계속)
user2685937

답변:


59

휘발성은 불필요합니다. 음, 일종의 **

volatile변수에 대한 읽기와 쓰기 사이에 메모리 장벽 *을 만드는 데 사용됩니다.
lock를 사용하면 블록에 lock대한 액세스를 하나의 스레드로 제한하는 것 외에도 내부 블록 주위에 메모리 장벽이 생성됩니다 .
메모리 장벽은 각 스레드가 변수의 최신 값 (일부 레지스터에 캐시 된 로컬 값이 아님)을 읽고 컴파일러가 명령문을 다시 정렬하지 않도록합니다. 사용은 volatile이미 잠금을 가지고 있기 때문에 ** 필요하지 않습니다.

Joseph Albahari 는 내가 할 수있는 것보다 더 잘 설명합니다.

그리고 C # 에서 싱글 톤 구현에 대한 Jon Skeet의 가이드 를 확인하십시오 .


update :
* volatile변수의 읽기는 VolatileReads가되고 쓰기는 s가되며 VolatileWrite, 이는 CLR의 x86 및 x64에서 MemoryBarrier. 다른 시스템에서는 더 세밀 할 수 있습니다.

** 내 대답은 x86 및 x64 프로세서에서 CLR을 사용하는 경우에만 정확합니다. Mono (및 기타 구현), Itanium64 및 향후 하드웨어와 같은 다른 메모리 모델에서도 마찬가지 일 있습니다. 이것이 Jon이 이중 체크 잠금에 대한 "gotchas"기사에서 언급 한 내용입니다.

약한 메모리 모델 상황에서 코드가 제대로 작동하려면 {변수를로 표시 하거나으로 volatileThread.VolatileRead거나에 대한 호출 삽입 Thread.MemoryBarrier} 중 하나를 수행 해야 할 수 있습니다.

내가 이해하는 바에 따르면 CLR (IA64에서도)에서는 쓰기가 다시 정렬되지 않습니다 (쓰기에는 항상 릴리스 의미가 있음). 그러나 IA64에서는 휘발성으로 표시되지 않는 한 읽기가 쓰기 전에 오도록 순서를 변경할 수 있습니다. 안타깝게도 IA64 하드웨어에 액세스 할 수 없으므로 이에 대해 제가 말하는 것은 추측 일 것입니다.

: 나는 또한 도움이 기사를 발견했습니다
http://www.codeproject.com/KB/tips/MemoryBarrier.aspx
밴스 모리슨의 기사 (이에 대한 모든 링크가 두 번 잠금 확인에 대해 이야기)
크리스 brumme의 기사 (이에 대한 모든 링크 )
Joe Duffy : 이중 검사 잠금의 깨진 변형

luis abreu의 멀티 스레딩 시리즈는 개념에 대한 멋진 개요도 제공합니다 .
http://msmvps.com/blogs/luisabreu/archive/2009/06/29/multithreading-load-and-store-reordering.aspx
http : // msmvps. com / blogs / luisabreu / archive / 2009 / 07 / 03 / multithreading-introducing-memory-fences.aspx


Jon Skeet은 실제로 적절한 메모리 바리어를 생성하려면 휘발성 수정자가 필요하다고 말한 반면 첫 번째 링크 작성자는 잠금 (Monitor.Enter)이면 충분하다고 말합니다. 누가 실제로 맞습니까 ???
Konstantin

@Konstantin Jon이 Itanium 64 프로세서의 메모리 모델을 언급 한 것 같으므로이 경우 휘발성을 사용해야 할 수 있습니다. 그러나 x86 및 x64 프로세서에서는 휘발성이 필요하지 않습니다. 조금 후에 더 업데이트하겠습니다.
dan

잠금이 실제로 메모리 장벽을 만들고 메모리 장벽이 실제로 명령 순서와 캐시 무효화에 관한 것이라면 모든 프로세서에서 작동해야합니다. 어쨌든 그런 기본적인 것이 너무나 많은 혼란을 야기하는 것은 너무 이상합니다 ...
Konstantin

2
이 대답은 나에게 잘못된 것 같습니다. 어떤 플랫폼 에서든volatile 불필요한 경우 JIT가 해당 플랫폼 에서 메모리로드 를 최적화 할 수 없다는 의미 입니다. 나에게는 그럴 것 같지 않습니다. object s1 = syncRoot; object s2 = syncRoot;object s1 = syncRoot; object s2 = s1;
user541686 dec

1
CLR이 쓰기 순서를 변경하지 않더라도 (사실이 아닐지라도 그렇게함으로써 매우 좋은 최적화를 많이 수행 할 수 있음) 생성자 호출을 인라인하고 제자리에서 개체를 생성 할 수있는 한 여전히 버그가 있습니다. (반쯤 초기화 된 객체를 볼 수 있습니다). 기본 CPU가 사용하는 메모리 모델과 무관 합니다! Eric Lippert에 따르면 Intel의 CLR은 최소한 생성자 이후에 이러한 최적화를 거부하는 membarrier를 도입하지만 사양에서 요구하는 것은 아니며 ARM에서도 동일한 일이 발생한다고 믿지 않을 것입니다.
Voo

34

volatile필드 없이 구현하는 방법이 있습니다 . 설명하겠습니다 ...

잠금 외부에서 완전히 초기화되지 않은 인스턴스를 얻을 수 있도록 위험한 것은 잠금 내부의 메모리 액세스 재정렬이라고 생각합니다. 이것을 피하기 위해 나는 이것을한다 :

public sealed class Singleton
{
   private static Singleton instance;
   private static object syncRoot = new Object();

   private Singleton() {}

   public static Singleton Instance
   {
      get 
      {
         // very fast test, without implicit memory barriers or locks
         if (instance == null)
         {
            lock (syncRoot)
            {
               if (instance == null)
               {
                    var temp = new Singleton();

                    // ensures that the instance is well initialized,
                    // and only then, it assigns the static variable.
                    System.Threading.Thread.MemoryBarrier();
                    instance = temp;
               }
            }
         }

         return instance;
      }
   }
}

코드 이해

Singleton 클래스의 생성자 내부에 초기화 코드가 있다고 상상해보십시오. 새 객체의 주소로 필드를 설정 한 후 이러한 명령어의 순서가 변경되면 불완전한 인스턴스가있는 것입니다. 클래스에 다음 코드가 있다고 상상해보십시오.

private int _value;
public int Value { get { return this._value; } }

private Singleton()
{
    this._value = 1;
}

이제 new 연산자를 사용하여 생성자를 호출한다고 상상해보십시오.

instance = new Singleton();

다음 작업으로 확장 할 수 있습니다.

ptr = allocate memory for Singleton;
set ptr._value to 1;
set Singleton.instance to ptr;

이 지침을 다음과 같이 재정렬하면 어떻게됩니까?

ptr = allocate memory for Singleton;
set Singleton.instance to ptr;
set ptr._value to 1;

차이가 있습니까? 단일 스레드를 생각하면 아니오 . 여러 스레드를 생각하면 ... 스레드가 중단 된 직후 set instance to ptr:

ptr = allocate memory for Singleton;
set Singleton.instance to ptr;
-- thread interruped here, this can happen inside a lock --
set ptr._value to 1; -- Singleton.instance is not completelly initialized

이것이 메모리 액세스 재정렬을 허용하지 않음으로써 메모리 장벽이 피하는 것입니다.

ptr = allocate memory for Singleton;
set temp to ptr; // temp is a local variable (that is important)
set ptr._value to 1;
-- memory barrier... cannot reorder writes after this point, or reads before it --
-- Singleton.instance is still null --
set Singleton.instance to temp;

즐거운 코딩 되세요!


1
CLR이 개체가 초기화되기 전에 개체에 대한 액세스를 허용하는 경우 이는 보안 허점입니다. 공용 생성자 만 "SecureMode = 1"로 설정되고 인스턴스 메서드가이를 확인하는 권한있는 클래스를 상상해보십시오. 생성자를 실행하지 않고 이러한 인스턴스 메서드를 호출 할 수 있다면 보안 모델에서 벗어나 샌드 박싱을 위반할 수 있습니다.
MichaelGG

1
@MichaelGG : 설명하신 경우 해당 클래스가 여러 스레드를 지원하여 액세스 할 수 있다면 문제입니다. 생성자 호출이 지터에 의해 인라인되면 저장된 참조가 완전히 초기화되지 않은 인스턴스를 가리키는 방식으로 CPU가 명령을 재정렬 할 수 있습니다. 이것은 피할 수 있기 때문에 CLR 보안 문제가 아니며, 이러한 클래스의 생성자 내에서 연동, 메모리 장벽, 잠금 및 / 또는 휘발성 필드를 사용하는 것은 프로그래머의 책임입니다.
Miguel Angelo

2
ctor 내부의 장벽은 그것을 고치지 않습니다. CLR이 ctor가 완료되기 전에 새로 할당 된 개체에 대한 참조를 할당하고 membarrier를 삽입하지 않으면 다른 스레드가 반 초기화 된 개체에서 인스턴스 메서드를 실행할 수 있습니다.
MichaelGG 2014

이것은 ReSharper 2016/2017이 C #에서 DCL의 경우 제안하는 "대체 패턴"입니다. OTOH, Java 결과 new가 완전히 초기화 됨을 보장합니다 .
user2864740

나는 MS .net 구현이 해석자의 끝에 메모리 장벽을 배치한다는 것을 알고 있습니다.
Miguel Angelo

7

나는 사람이 실제로 응답 한 생각하지 않는다 질문을 나는 그것을 시도 줄거야, 그래서.

휘발성과 첫 번째 if (instance == null)는 "필요"하지 않습니다. 잠금은이 코드를 스레드로부터 안전하게 만듭니다.

그래서 질문은 : 왜 첫 번째를 추가 if (instance == null)하겠습니까?

그 이유는 아마도 코드의 잠긴 부분을 불필요하게 실행하지 않기 위해서 일 것입니다. 잠금 내부에서 코드를 실행하는 동안 해당 코드를 실행하려는 다른 스레드도 차단되므로 여러 스레드에서 싱글 톤에 자주 액세스하려고하면 프로그램 속도가 느려집니다. 언어 / 플랫폼에 따라 피하고 싶은 잠금 자체의 오버 헤드가있을 수도 있습니다.

따라서 첫 번째 null 검사는 잠금이 필요한지 확인하는 매우 빠른 방법으로 추가됩니다. 싱글 톤을 생성 할 필요가 없다면 잠금을 완전히 피할 수 있습니다.

그러나 참조가 어떤 식 으로든 잠그지 않고는 null인지 확인할 수 없습니다. 프로세서 캐싱으로 인해 다른 스레드가이를 변경할 수 있으며 불필요하게 잠금을 입력하도록 유도하는 "부실"값을 읽을 수 있기 때문입니다. 하지만 당신은 자물쇠를 피하려고합니다!

따라서 잠금을 사용할 필요없이 최신 값을 읽을 수 있도록 싱글 톤을 휘발성으로 만듭니다.

volatile은 변수에 대한 단일 액세스 중에 만 사용자를 보호하기 때문에 여전히 내부 잠금이 필요합니다. 잠금을 사용하지 않고는 안전하게 테스트하고 설정할 수 없습니다.

자, 이것이 실제로 유용합니까?

저는 "대부분의 경우에는 아니오"라고 말할 것입니다.

Singleton.Instance가 잠금으로 인해 비 효율성을 유발할 수 있다면 왜 그렇게 자주 호출하여 이것이 심각한 문제가 될까요? 싱글 톤의 요점은 하나만 있으므로 코드가 싱글 톤 참조를 한 번 읽고 캐시 할 수 있다는 것입니다.

이 캐싱이 가능하지 않은 유일한 경우는 많은 수의 스레드가있을 때입니다 (예 : 모든 요청을 처리하기 위해 새 스레드를 사용하는 서버는 수백만 개의 매우 짧은 스레드를 생성 할 수 있습니다. Singleton.Instance를 한 번 호출해야 함).

그래서 저는 이중 체크 잠금이 성능이 매우 중요한 경우에 실제 위치를 차지하는 메커니즘이라고 생각합니다. 그리고 모든 사람들은 그것이 무엇을하는지 여부를 실제로 생각하지 않고 "이것이 올바른 방법입니다"라는 악 대차에 올라 섰습니다. 실제로 사용하는 경우에 필요합니다.


6
이것은 잘못된 것과 요점을 놓치는 것 사이에 있습니다. volatile이중 검사 잠금의 잠금 의미와 관련이 없으며 메모리 모델 및 캐시 일관성과 관련이 있습니다. 그 목적은 한 스레드가 다른 스레드에 의해 아직 초기화되고있는 값을받지 않도록하는 것입니다.이 값은 이중 검사 잠금 패턴이 본질적으로 방지하지 않습니다. Java에서는 반드시 volatile키워드 가 필요합니다 . .NET에서는 ECMA에 따르면 잘못되었지만 런타임에 따라 옳기 때문에 어둡습니다. 어느 쪽이든, lock확실히 그것을 처리 하지 않습니다 .
Aaronaught

어? 나는 당신의 진술이 내가 말한 것과 일치하지 않는 부분을 볼 수 없으며 휘발성이 잠금 의미와 관련이 있다고 말하지도 않았습니다.
Jason Williams

6
이 스레드의 다른 여러 문장과 마찬가지로 귀하의 답변 lock은 코드가 스레드로부터 안전하다고 주장합니다 . 그 부분은 사실 이지만 재확인 잠금 패턴으로 인해 안전하지 않을 수 있습니다 . 그것이 당신이 놓치고있는 것 같습니다. 이 답변은 .NET의 이유 인 스레드 안전 문제를 해결하지 않고 이중 확인 잠금의 의미와 목적에 대해 의미가있는 것 같습니다 volatile.
Aaronaught 2011

1
instance가 표시된 경우 어떻게 안전하지 않게 만들 수 volatile있습니까?
UserControl

5

이중 확인 잠금 패턴과 함께 휘발성을 사용해야합니다.

대부분의 사람들은이 기사를 휘발성이 필요하지 않다는 증거로 지적합니다. https://msdn.microsoft.com/en-us/magazine/cc163715.aspx#S10

그러나 그들은 끝까지 읽지 못합니다. " 경고의 마지막 단어-저는 기존 프로세서에서 관찰 된 동작에서 x86 메모리 모델을 추측하고 있습니다. 따라서 하드웨어와 컴파일러가 시간이 지남에 따라 더 공격적이 될 수 있기 때문에 로우 록 기술도 취약합니다. . 다음은 이러한 취약성이 코드에 미치는 영향을 최소화하기위한 몇 가지 전략입니다. 첫째, 가능할 때마다 로우 락 기술을 피하십시오. (...) 마지막으로, 암시 적 보장에 의존하는 대신 휘발성 선언을 사용하여 가능한 가장 약한 메모리 모델을 가정합니다. . "

더 설득력이 필요한 경우 ECMA 사양에 대한이 기사를 읽으십시오. msdn.microsoft.com/en-us/magazine/jj863136.aspx

좀 더 설득력이 필요한 경우 휘발성없이 작동하지 않도록 최적화를 적용 할 수 있다는 최신 기사를 읽으십시오. msdn.microsoft.com/en-us/magazine/jj883956.aspx

요약하자면 당분간 휘발성없이 작동 할 수 있지만 적절한 코드를 작성하고 volatile 또는 volatile 읽기 / 쓰기 메서드를 사용할 가능성은 없습니다. 다른 방법을 제안하는 기사에서는 코드에 영향을 줄 수있는 JIT / 컴파일러 최적화의 가능한 위험과 코드를 손상시킬 수있는 향후 최적화를 생략하는 경우가 있습니다. 또한 지난 기사에서 언급했듯이 휘발성없이 작동한다는 이전 가정은 이미 ARM에 적용되지 않을 수 있습니다.


1
좋은 대답입니다. 이 질문에 대한 유일한 정답은 "아니오"입니다. 이것에 따르면 받아 들여지는 대답은 잘못되었습니다.
Dennis Kassel

3

AFAIK (그리고-조심스럽게 이것을 취하십시오. 나는 많은 동시 작업을하지 않습니다) 아니오. 잠금은 여러 경쟁자 (스레드) 간의 동기화를 제공합니다.

반면에 volatile은 캐시 된 (그리고 잘못된) 값을 발견하지 않도록 매번 값을 재평가하도록 컴퓨터에 지시합니다.

http://msdn.microsoft.com/en-us/library/ms998558.aspx를 참조 하고 다음 인용문을 참고하십시오.

또한 변수는 인스턴스 변수에 액세스하기 전에 인스턴스 변수에 대한 할당이 완료되도록 휘발성으로 선언됩니다.

휘발성에 대한 설명 : http://msdn.microsoft.com/en-us/library/x13ttww7%28VS.71%29.aspx


2
'잠금'은 또한 휘발성과 동일하거나 더 나은 메모리 장벽을 제공합니다.
Henk Holterman

2

내가 찾고 있던 것을 찾았다 고 생각합니다. 자세한 내용은이 기사 ( http://msdn.microsoft.com/en-us/magazine/cc163715.aspx#S10)에 있습니다.

요약하면 .NET에서 휘발성 수정자는 실제로이 상황에서 필요하지 않습니다. 그러나 약한 메모리 모델에서는 지연 시작된 객체의 생성자에서 작성된 쓰기가 필드에 쓴 후 지연 될 수 있으므로 다른 스레드가 첫 번째 if 문에서 손상된 null이 아닌 인스턴스를 읽을 수 있습니다.


1
그 기사의 맨 아래에서 특히 저자가 마지막으로 언급 한 문장을주의 깊게 읽으십시오. "경고의 마지막 단어-기존 프로세서에서 관찰 된 동작에서 x86 메모리 모델을 추측하고 있습니다. 따라서 로우 록 기술도 취약하기 때문입니다. 하드웨어와 컴파일러는 시간이 지남에 따라 더욱 공격적으로 변할 수 있습니다. 다음은 이러한 취약성이 코드에 미치는 영향을 최소화하기위한 몇 가지 전략입니다. 첫째, 가능하면 로우 록 기술을 피하십시오. (...) 마지막으로 가능한 가장 약한 메모리 모델을 가정합니다. 암시 적 보장에 의존하는 대신 휘발성 선언을 사용합니다. "
user2685937

1
더 설득력이 필요한 경우 ECMA 사양에 대한이 기사를 읽으십시오. 다른 플랫폼에도 사용됩니다. msdn.microsoft.com/en-us/magazine/jj863136.aspx 더 설득력이 필요한 경우 최적화가 포함될 수 있다는 최신 기사를 읽으십시오. 휘발성없이 작동하지 않도록 방지합니다. msdn.microsoft.com/en-us/magazine/jj883956.aspx 요약하자면 당분간 휘발성없이 작동 할 수 있지만 적절한 코드를 작성하고 사용하지 마십시오. 휘발성 또는 휘발성 읽기 / 쓰기 방법.
user2685937

1

lock충분하다. MS 언어 사양 (3.0) 자체는 §8.12에서이 정확한 시나리오를 언급합니다 volatile.

더 나은 방법은 개인용 정적 개체를 잠가 정적 데이터에 대한 액세스를 동기화하는 것입니다. 예를 들면 :

class Cache
{
    private static object synchronizationObject = new object();
    public static void Add(object x) {
        lock (Cache.synchronizationObject) {
          ...
        }
    }
    public static void Remove(object x) {
        lock (Cache.synchronizationObject) {
          ...
        }
    }
}

그의 기사 ( yoda.arachsys.com/csharp/singleton.html ) 에서 Jon Skeet 은이 경우 적절한 메모리 장벽을 위해 휘발성이 필요하다고 말합니다. 마크, 이것에 대해 언급 할 수 있습니까?
Konstantin

아, 두 번 확인 된 자물쇠를 알아 차리지 못했습니다. 간단히 : 그렇게하지 마세요 ;-p
Marc Gravell

실제로 이중 확인 잠금이 성능 측면에서 좋은 것이라고 생각합니다. 또한 필드를 휘발성으로 만들어야하는 경우 잠금 내에서 액세스하는 동안 이중 첵 잠금은 다른 잠금보다 훨씬 나쁘지 않습니다 ...
Konstantin

그러나 Jon이 언급 한 별도의 클래스 접근 방식만큼 좋은가요?
Marc Gravell

-3

이것은 이중 체크 잠금과 함께 휘발성을 사용하는 것에 대한 꽤 좋은 게시물입니다.

http://tech.puredanger.com/2007/06/15/double-checked-locking/

Java에서 목표가 변수를 보호하는 것이라면 휘발성으로 표시된 경우 잠글 필요가 없습니다.


3
흥미롭지 만 반드시 도움이되는 것은 아닙니다. JVM의 메모리 모델과 CLR의 메모리 모델은 동일하지 않습니다.
bcat
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.