.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"기사에서 언급 한 내용입니다.

약한 메모리 모델 상황에서 코드가 제대로 작동하려면 {변수를로 표시 하거나으로 volatile읽 Thread.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.