5

그래서 여러 스레드가 지연 생성 된 싱글 톤을 초기화하지 못하도록하는 C++ 이중 검사 잠금이 깨 졌다고 주장하는 기사가 많이 있습니다. 보통 이중 확인 잠금 코드는 다음과 같이 읽습니다이중 확인 잠금에 대한이 수정 프로그램의 문제점은 무엇입니까?

class singleton { 
private: 
    singleton(); // private constructor so users must call instance() 
    static boost::mutex _init_mutex; 

public: 
    static singleton & instance() 
    { 
     static singleton* instance; 

     if(!instance) 
     { 
      boost::mutex::scoped_lock lock(_init_mutex); 

      if(!instance)   
       instance = new singleton; 
     } 

     return *instance; 
    } 
}; 

문제는 분명히 선 할당 인스턴스 - 컴파일러가 포인터를 할당하거나 포인터를 설정 한 후 객체를 할당하고 무료입니다 할당되고 할당됩니다. 후자의 경우는 관용구를 깨뜨린다. 하나의 쓰레드는 메모리를 할당하고 포인터를 할당하지만 싱글 톤의 생성자를 실행하기 전에 실행하지 않는다. 그런 다음 두 번째 쓰레드는 인스턴스가 null이 아니며 그것을 반환하려고 시도한다. 비록 그것이 아직 구축되지 않았지만.

I saw a suggestion 스레드 로컬 부울을 사용하고 instance 대신이를 확인하십시오. 이런 식으로 뭔가 :

class singleton { 
private: 
    singleton(); // private constructor so users must call instance() 
    static boost::mutex _init_mutex; 
    static boost::thread_specific_ptr<int> _sync_check; 

public: 
    static singleton & instance() 
    { 
     static singleton* instance; 

     if(!_sync_check.get()) 
     { 
      boost::mutex::scoped_lock lock(_init_mutex); 

      if(!instance)   
       instance = new singleton; 

      // Any non-null value would work, we're really just using it as a 
      // thread specific bool. 
      _sync_check = reinterpret_cast<int*>(1); 
     } 

     return *instance; 
    } 
}; 

약간의 성능 저하를 수반하지만, 모든 전화를 잠금으로 여전히 거의 그렇게 나쁘지 각 스레드가 인스턴스가 한 번 생성 된 경우 확인 끝납니다 만, 그 후 정지 이런 식으로. 하지만 로컬 스태커를 방금 사용하면 어떨까요? :

class singleton { 
private: 
    singleton(); // private constructor so users must call instance() 
    static boost::mutex _init_mutex; 

public: 
    static singleton & instance() 
    { 
     static bool sync_check = false; 
     static singleton* instance; 

     if(!sync_check) 
     { 
      boost::mutex::scoped_lock lock(_init_mutex); 

      if(!instance)   
       instance = new singleton; 

      sync_check = true; 
     } 

     return *instance; 
    } 
}; 

왜이 기능이 작동하지 않습니까? sync_check가 다른 스레드에 할당 될 때 한 스레드가 sync_check를 읽을지라도 가비지 값은 여전히 ​​0이 아니므로 참입니다. This Dr. Dobb's article은 명령을 재정렬하는 것보다 컴파일러와의 전투에서 결코 이기지 않으므로 잠 가야한다고 주장합니다. 그래서 어떤 이유로 든 작동하지 않아야한다고 생각 하죠.하지만 그 이유를 알 수는 없어요. 시퀀스 포인트에 대한 요구 사항이 Dr. Dobb의 기사만큼이나 손실된다고 생각하면 왜 코드가 자물쇠 앞에 정렬되지 않았는지 이해할 수 없습니다. C++ 멀티 스레딩이 깨진 것입니다.

로컬 변수이기 때문에 sync_check를 특별히 재정렬 할 수있는 컴파일러를 볼 수있을 것 같아요 (정적인데도 참조 또는 포인터를 반환하지는 않습니다) -하지만이 경우 대신 정적 멤버 (효과적으로 글로벌)로 만들어 해결할 수 있습니다.

이렇게 작동합니까, 그렇지 않습니까? 왜?

+2

문제는 개체가 할당되기 전에가 아니라 생성자가 실행 (또는 완료)되기 전에 변수를 할당 할 수 있다는 것입니다. – kdgregory

+0

감사합니다. 나는 경쟁 조건을 완전히 망각했다. –

+1

네, 맞습니다, 현재 C++은 실제로 "멀티 쓰레드 깨진 마침표"입니다. 표준으로 만 고려할 때. 보통 컴파일러 벤더들은 이것에 대한 방법을 제공합니다. 따라서 실용적인 결과는 그렇게 끔찍한 것이 아닙니다. – Suma

답변

5

sync_check에 대한 쓰기와 인스턴스가 CPU에서 순서가 맞지 않아서 수정 사항이 수정되지 않습니다. 예를 들어 인스턴스에 대한 처음 두 번의 호출이 두 개의 서로 다른 CPU에서 거의 동시에 발생한다고 상상해보십시오. 첫 번째 스레드는 잠금을 획득하고 포인터를 초기화하고 sync_check를 true로 설정합니다. 프로세서는 메모리에 쓰기 순서를 변경할 수 있습니다. 다른 CPU에서는 두 번째 스레드가 sync_check를 검사하여 참임을 알지만 인스턴스는 아직 메모리에 기록되지 않을 수 있습니다. 자세한 내용은 Lockless Programming Considerations for Xbox 360 and Microsoft Windows을 참조하십시오.

사용자가 언급 한 스레드 특정 sync_check 솔루션이 작동합니다 (포인터를 0으로 초기화한다고 가정).

+0

마지막 문장과 관련하여 : 예, 확실하지 않지만 thread_specific_ptr이 내부적으로 뮤텍스를 사용한다고 생각합니다. 그렇다면 뮤텍스를 잠그는 것 (이중 잠금 없음)과 비교할 때 그 솔루션을 사용하는 것이 무엇이겠습니까? – n1ckp

1

이에 대한 몇 가지 좋은 독서 여기 (이 # 지향 .NET/C 비록)있다 : http://msdn.microsoft.com/en-us/magazine/cc163715.aspx

그것은 무엇으로 귀결하는 것은 당신이 순서를 변경할 수없는 CPU를 말할 수 있어야한다는 것입니다 귀하의 읽기/쓰기는 이 가변 액세스 (원래 펜티엄 이후 CPU가 논리가 영향을받지 않을 것이라고 생각하면 CPU가 특정 명령어를 재정렬 할 수 있음)와 캐시가 일관성이 있는지 확인해야합니다 (이를 잊지 마십시오. 우리는 devs 모든 메모리는 단순한 리소스이지만 모든 CPU 코어는 캐시를 가지고있는 반면, 공유되지 않은 (L1) 공유 가끔 있습니다 (L2)) - 초기화는 메인 RAM에 기록 할 수 있지만 다른 코어 캐시에서 초기화되지 않은 값을 가질 수 있습니다. 동시성 의미론이 없다면 CPU는 캐시가 더럽다는 것을 모를 수 있습니다.

C++ 측면을 모르지만 .net에서는 변수를 volatile로 지정하여 액세스를 보호합니다 (또는 System.Threading의 메모리 읽기/쓰기 차단 메서드 사용).

제쳐두고, 나는 .net 2.0에서 이중 잠금 검사가 "휘발성"변수없이 작동한다는 것을 확인했습니다. (.net 독자의 경우) - C++에 도움이되지 않습니다. 암호.

안전하고 싶다면 C#에서 변수를 휘발성으로 표시하는 것과 동일한 기능을 수행해야합니다.

+1

C++ 변수는 휘발성으로 선언 될 수 있지만 C#과 동일한 의미를 갖는 것은 의심 스럽습니다. 나는 또한 어딘가에서 이것이 휘발성의 남용이라고 읽었던 것을 기억하지만 왜 그런지 기억하지 못해서 그 기사가 얼마나 합리적이라고 판단 할 수는 없다. –

+0

다른 언어에서는 남용 (C#에서도 악용 될 수 있음) 일 수 있습니다. 낮은 잠금 또는 잠금 해제 코드를 작성하는 것이 어려운 측면 중 하나는 지침의 불일치입니다. 나는 그것에 대해 읽는 데 시간을 보냈으며 마이크로 소프트 내에서도 블로거 중 일부는 메모리 울타리가 필요할 때와 휘발성을 사용해야 할 때 서로 모순되는 것처럼 보입니다. 확실히 어려운 문제입니다. – JMarsch

+0

현재 C++에는 .NET volatile에 해당하는 것이 없습니다 (표준에 정의 된대로). C++ 0x 표준이 곧 발표 할 영역 중 하나입니다. 한편 컴파일러가 제공하는 것을 사용해야합니다 (Visual Studio에서는 휘발성 메모리 펜스). – Suma

0

"후자의 경우는 이디엄을 깨뜨립니다. 두 스레드가 결국 싱글 톤을 생성하게됩니다."

하지만 첫 번째 예제에서는 인스턴스가 이미 존재하는지 확인합니다 (동시에 여러 스레드에서 실행될 수 있음). 한 스레드가 스레드를 잠그고 스레드를 생성하지 않으면 인스턴스 - 한 번에 하나의 스레드 만 생성 할 수 있습니다. 다른 모든 스레드는 잠기고 대기합니다.

인스턴스가 만들어지고 뮤텍스가 잠금 해제되면 다음 대기중인 스레드는 뮤텍스를 잠그지 만 검사가 실패하므로 새 인스턴스를 만들지 않습니다.

다음에 인스턴스 변수가 확인되면 스레드가 새 인스턴스를 만들려고하지 않도록 설정됩니다.

다른 스레드가 동일한 변수를 검사하는 동안 하나의 스레드가 새 인스턴스 포인터를 인스턴스에 할당하는 경우에는 확실하지 않지만이 경우에는 올바르게 처리됩니다.

여기에 뭔가가 있습니까?

작업의 순서를 재지 정하는 것은 확실하지 않지만이 경우에는 로직이 변경 될 것이므로 그렇게 될 것으로 기대하지는 않습니다.하지만이 주제에 대해서는 전문가가 아닙니다.

+0

맞습니다. 실제 경쟁 조건에 대해서는 틀 렸습니다. 문제는 두 번째 스레드가 인스턴스가 null이 아니며 첫 번째 스레드가 스레드를 구성하기 전에 반환하려고한다는 것입니다. 내 게시물을 수정했습니다. –