문제

그래서 저는 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이 아닌 것을 확인하고 이를 반환하려고 시도합니다. , 아직 구축되지 않았지만.

제안을 봤어 스레드 로컬 부울을 사용하고 대신 확인하려면 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;
    }
};

이런 방식으로 각 스레드는 인스턴스가 한 번 생성되었는지 확인하지만 그 후에는 중지됩니다. 이로 인해 약간의 성능 저하가 수반되지만 여전히 모든 호출을 잠그는 것만큼 나쁘지는 않습니다.하지만 방금 로컬 정적 bool을 사용했다면 어떨까요?:

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를 읽어도 가비지 값은 여전히 ​​0이 아니므로 true입니다. 이 박사.Dobb의 기사 명령 재정렬을 놓고 컴파일러와의 싸움에서 결코 이길 수 없기 때문에 잠가야 한다고 주장합니다.어떤 이유로 이것이 작동하지 않아야한다고 생각하지만 그 이유를 알 수 없습니다.시퀀스 포인트에 대한 요구 사항이 Dr.Dobb의 기사를 보면 믿게 되는데, 왜 그런지 모르겠습니다. 어느 잠금 뒤의 코드는 잠금 앞의 코드로 재정렬될 수 없습니다.이로 인해 C++ 멀티스레딩 기간이 중단되었습니다.

컴파일러가 sync_check가 로컬 변수이기 때문에 잠금 앞에 있도록 특별히 재정렬할 수 있는 것을 볼 수 있을 것 같습니다(정적임에도 참조나 포인터를 반환하지 않음). 하지만 이 문제는 여전히 해결될 수 있습니다. 대신 정적 멤버(효과적으로 전역)로 만듭니다.

그럼 이게 먹힐까, 안 먹힐까?왜?

도움이 되었습니까?

해결책

sync_check 및 인스턴스에 대한 쓰기가 CPU에서 순서대로 수행될 수 있으므로 수정 사항은 아무것도 수정하지 않습니다.예를 들어 인스턴스에 대한 처음 두 호출이 서로 다른 두 CPU에서 거의 동시에 발생한다고 가정해 보세요.첫 번째 스레드는 잠금을 획득하고, 포인터를 초기화하고, sync_check를 true로 설정하지만, 프로세서는 메모리에 쓰기 순서를 변경할 수 있습니다.다른 CPU에서는 두 번째 스레드가 sync_check를 검사하여 이것이 사실인지 확인하는 것이 가능하지만 인스턴스가 아직 메모리에 기록되지 않았을 수 있습니다.보다 Xbox 360 및 Microsoft Windows에 대한 잠금 없는 프로그래밍 고려 사항 자세한 내용은.

그러면 언급한 스레드별 sync_check 솔루션이 작동할 것입니다(포인터를 0으로 초기화한다고 가정).

다른 팁

이에 대한 훌륭한 자료가 있습니다(비록 .net/c# 지향이지만). http://msdn.microsoft.com/en-us/magazine/cc163715.aspx

요점은 이 변수 ​​액세스에 대한 읽기/쓰기 순서를 변경할 수 없음을 CPU에 알릴 수 있어야 한다는 것입니다(원래 Pentium 이후로 CPU는 논리가 영향을 받지 않을 것이라고 생각하는 경우 특정 명령을 재정렬할 수 있습니다). ) 그리고 캐시의 일관성을 보장해야 합니다(잊지 마세요. 개발자는 모든 메모리가 하나의 플랫 리소스인 것처럼 가정하지만 실제로는 각 CPU 코어에 캐시가 있고 일부는 공유되지 않음(L1) ), 일부는 때때로 공유될 수 있습니다(L2)) - 초기화가 기본 RAM에 기록될 수 있지만 다른 코어에는 캐시에 초기화되지 않은 값이 있을 수 있습니다.동시성 의미 체계가 없으면 CPU는 캐시가 더티하다는 것을 알지 못할 수 있습니다.

C++ 측면은 모르지만 .net에서는 변수에 대한 액세스를 보호하기 위해 변수를 휘발성으로 지정합니다(또는 System.Threading에서 메모리 읽기/쓰기 장벽 메서드를 사용합니다).

여담으로, 나는 .net 2.0에서 이중 확인 잠금이 "휘발성" 변수(모든 .net 리더에 대해) 없이 작동하도록 보장된다는 것을 읽었습니다. 이는 C++ 코드에 도움이 되지 않습니다.

안전을 유지하려면 C#에서 변수를 휘발성으로 표시하는 것과 동일한 C++ 작업을 수행해야 합니다.

"후자의 경우 관용구가 깨졌습니다. 두 개의 스레드가 결국 싱글톤을 생성하게 될 수도 있습니다."

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

인스턴스가 생성되고 뮤텍스가 잠금 해제되면 다음 대기 스레드가 뮤텍스를 잠그지만 확인이 실패하므로 새 인스턴스 생성을 시도하지 않습니다.

다음에 인스턴스 변수를 검사하면 스레드가 새 인스턴스를 생성하려고 시도하지 않도록 설정됩니다.

한 스레드가 인스턴스에 새 인스턴스 포인터를 할당하는 동안 다른 스레드가 동일한 변수를 확인하는 경우에 대해서는 잘 모르겠지만 이 경우에는 올바르게 처리될 것이라고 믿습니다.

여기서 뭔가 빠졌나요?

작업 순서 변경에 대해서는 잘 모르겠지만 이 경우 논리가 변경되므로 그런 일이 발생할 것으로 예상하지는 않습니다. 하지만 저는 이 주제에 대한 전문가가 아닙니다.

라이센스 : CC-BY-SA ~와 함께 속성
제휴하지 않습니다 StackOverflow
scroll top