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

여러 텍스트는 .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)”이 필요한 메모리 일관성을 달성하지 못하는 이유는 무엇입니까? “잠금”문 이후 읽기와 쓰기가 모두 휘발성이므로 필요한 일관성이 달성된다는 것이 사실이 아닙니까?



답변

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

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


답변

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;

즐거운 코딩 되세요!


답변

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

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

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

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

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

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

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

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

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

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

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

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

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


답변

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

대부분의 사람들은이 기사를 휘발성이 필요하지 않다는 증거로 지적합니다.
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에 적용되지 않을 수 있습니다.


답변

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

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

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

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

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


답변

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

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


답변

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) {
          ...
        }
    }
}