이 문서에서는 Singleton 디자인 패턴에 대한 몇가지 이슈를 소개합니다. Singleton 패턴은 클래스의 인스턴스가 단 하나만 생성되도록 제한하기 위한 패턴입니다. “GoF의 디자인패턴”에서는 “클래스가 단 하나의 인스턴스만 가지는 것을 보장하고, 전역에서 그 인스턴스에 접근할 수 있도록 지원한다.”라고 설명합니다.
Singleton 패턴은 보기보다 간단하지 않고 구현에 대한 많은 논의가 존재합니다. 그 중에 C++11을 기반으로 한 몇몇의 구현에 대해 정리하고자 합니다.
기본 아이디어는 Singleton 클래스를 private인 static 인스턴스로 구현하고 생성자 역시 private이며 인터페이스 메서드가 static 인스턴스를 리턴하는 것으로 하겠습니다.
Version1
대부분의 일반적인 접근 방식은 아래와 같을 것입니다.
class simpleSingleton { simpleSingleton(); static simpleSingleton* _pInstance; public: ~simpleSingleton() { } static simpleSingleton* getInstance() { if(!_pInstance) { _pInstance = new simpleSingleton(); } return _pInstance; } void demo() { std::cout << "simple singleton # next - your code ..." << std::endl; } }; simpleSingleton* simpleSingleton::_pInstance = nullptr;
아쉽게도 이러한 방법은 여러가지 문제점을 안고 있습니다. 기본 생성자가 private이지만 복사 생성자가 할당 연산자가 컴파일 타임에 정의되기 때문에 다음과 같은 동작이 성립하게 됩니다.
// Version 1 simpleSingleton * p = simpleSingleton::getInstance(); // cache instance pointer p->demo(); // Version 2 simpleSingleton::getInstance()->demo(); simpleSingleton ob2(*p); // copy constructor ob2.demo(); simpleSingleton ob3 = ob2; // copy constructor ob2.demo();
따라서 복사 생성자와 할당 연산자도 명시적으로 private를 정의해 줘야 합니다.
Version2
Scott Meyers의 Effective C++에서 위의 버전을 약간 개선하고 getInstance()함수가 포인터가 아닌 레퍼런스를 리턴하도록 수정한 것을 소개했습니다. 따라서 포인터를 최종적으로 제거해야 하는 이슈가 사라졌죠.
이 방식의 한가지 장점은 함수 내부에서 static 객체를 초기화 하는 것은 함수를 처음 호출하는 시점이라는 것입니다.
class otherSingleton { static otherSingleton * pInstance; otherSingleton (); otherSingleton(const otherSingleton& rs) { pInstance = rs.pInstance; } otherSingleton& operator = (const otherSingleton& rs) { if (this != &rs) { pInstance = rs.pInstance; } return *this; } ~otherSingleton (); public: static otherSingleton& getInstance() { static otherSingleton theInstance; pInstance = &theInstance; return *pInstance; } void demo() { std::cout << "other singleton # next - your code ..." << std::endl; } }; otherSingleton * otherSingleton::pInstance = nullptr;
소멸자를 private로 두어 클라이언트에서 객체의 포인터를 이용해 실수로 delete를 수행하는 것을 막았습니다. 따라서 객체의 복제가 생성되는 것을 하용하지 않습니다.
otherSingleton ob = *p; ob.demo(); error C2248: otherSingleton::otherSingleton ' : cannot access private member declared in class 'otherSingleton' error C2248: 'otherSingleton::~otherSingleton' : cannot access private member declared in class 'otherSingleton'
하지만 다음과 같은 동작이 가능합니다.
// Version 1 otherSingleton *p = & otherSingleton::getInstance(); // cache instance pointer p->demo(); // Version 2 otherSingleton::getInstance().demo();
이런 구현 방식은 쓰레드 안정성을 보장하지 못합니다.
Multi-threaded environment
위의 두 방식은 싱글쓰레드 어플리케이션에서는 괜찮을 수 있지만 멀티쓰레드 환경을 고려하기에는 어렵습니다. Raymond Chen은 왜 C++ static이 Thread-safe하지 않은지를 설명합니다. 공유된 전역 리소스은 레이스컨디션이나 여러가지 쓰레딩 이슈를 유발합니다. 따라서 위의 Singleton 객체는 이러한 이슈에 버틸수가 없습니다. 다음과 같은 상황이 멀티쓰레드 환경에서 일어난다고 생각해 봅시다.
static simpleSingleton* getInstance() { if(!pInstance) // 1 { pInstance = new simpleSingleton(); // 2 } return pInstance; // 3 }
첫번째 쓰레드에서 getInstance()를 호출하면 pInstance가 null입니다. 쓰레드는 (2)에 도달하고 new를 호출할 준비를 하고 있습니다. 바로 그 때!! OS 스케쥴러가 인터럽트를 걸고 제어권을 다른 쓰레드에 넘겨버립니다. 두번째 쓰레드는 같은 과정을 수행하여 new를 호출하고 pInstance에 할당하여 그 포인터를 가져 갑니다. 다시 첫번째 쓰레드에게 기회가 돌아오면 아까 준비하고 있던 new를 호출하고 또 다시 pInstance에 할당하여 그 포인터를 가져 갑니다. 이제 우리는 하나가 아닌 두개의 Singleton 객체를 가지게 되었고, 심지어 메모리릭을 발생시킬 겁니다. 두 쓰레드는 서로 다른 인스턴스를 가지게 되었네요.
이러한 상황을 개선하기 위해서 C++11에 추가된 쓰레드 락 메커니즘을 사용해 봅시다. Scott Meyers의 구현은 아래와 같습니다.
static otherSingleton& getInstance() { std::lock_guard<std::mutex> lock(_mutex); static otherSingleton theInstance; pInstance = &theInstance; return *pInstance; }
std::lock_guard는 생성자에서 뮤텍스에 락을 걸고 소멸자에서 락을 해제 합니다. _mutex가 락이 걸려 있을 때는 다른 쓰레드가 접근할 수 없죠. 하짐나 이 구현방법은 getInstance를 호출할 때 마다 불필요한 동기화 관련 오버헤드를 감수해야 합니다. Singleton객체를 필요로 할 때마다 락을 걸고 있는데 실상 락은 최초로 pInstance를 초기화 할때만 필요합니다. getInstance()가 몇번 호출되든지 락은 최초 한번만 동작해 주는게 필요합니다.
C++은 쓰레드 관련 표준 지원이 없었기 때문에 Singleton이 확실하게 쓰레드 안정성 보장하는 것은 간단한 문제가 아니었습니다.
Singleton이 쓰레드 안정성을 보장하기 위해서는 double-checked locking(DCLP) 패턴을 적용해야 합니다. 이 패턴은 동기화 코드에 진입하기 전에 조건 체크를 한번 더 수행합니다. 그래서 Version1을 임시객체를 사용하도록 재작성했습니다.
static simpleSingleton* getInstance() { if (!pInstance) { std::lock_guard<std::mutex> lock(_mutex); if (!pInstance) { simpleSingleton * temp = new simpleSingleton; pInstance = temp; } } return pInstance; }
이 패턴은 락을 걸기 전에 pInstance가 null인지를 체크하고 null이라면 락을 걸어준 후에 다시한번 pInstance가 null인지를 확인합니다. 두번째 테스트는 여러 쓰레드에서 pInstance를 초기화 함과 동시에 null체크를 수행하는 레이스컨디션 때문에 필요합니다.
이론적으로 이 패턴은 옳지만 실상 언제나 제대로 동작하지는 않습니다. 특히 멀티프로세서 환경에서 말이죠.
하나의 프로세서가 메모리에 접근하여 쓰기 동작의 재정렬을 수행하는 동안 다른 프로세서에서 같은 메모리를 참조하는 동작은 제대로 수행되지 않습니다. 이 말은 Singleton객체가 완전히 초기화 되기 이전에 pInstance가 할당되는 동작이 일어날 수 있다는 것입니다.
첫번째 getInstance()의 호출 이후에 pointer를 이용한 구현은 메모리 릭을 위한 포인터를 필요로 하게 됩니다.
Version3 - Singleton with smart pointers
C++11 이전에는 C++ 표준에 쓰레딩 모델을 지원하지 않아 POSIX나 OS API 같은 외부 API를 사용해야 했습니다. 하지만 C++11에서 쓰레딩 모델을 표준으로 지원하기 시작했습니다. 불행히도 Visual Studio 2010에서의 C++ 표준은 쓰레드를 완전히 지원하지 못하는 버전입니다. 하지만 Visual Studio 2012 부터는 완전히 지원하기 시작했습니다.
class smartSingleton { private: static std::mutex _mutex; static std::weak_ptr<smartSingleton> thisObjPtr; smartSingleton(); smartSingleton(const smartSingleton& rs); smartSingleton& operator = (const smartSingleton& rs); public: ~smartSingleton(); static std::shared_ptr<smartSingleton>& getInstance() { static std::shared_ptr<smartSingleton> instance = thisObjPtr.lock(); if (!instance) { std::lock_guard<std::mutex> lock(_mutex); if (!instance) { instance.reset(new smartSingleton()); thisObjPtr = instance; } } return instance; } void demo() { std::cout << "smart pointers # next - your code ..." << std::endl; } }; std::weak_ptr< smartSingleton > smartSingleton:: thisObjPtr = nullptr;
알다시피 C++ 클래스의 기본 접근자는 private 입니다. 따라서 우리의 기본 생성자 역시 private 입니다. 결국 우리는 Singleton 인스턴를 쓰레드 걱정없이 사용할 수 있게 된 듯 합니다.
// Version 1 std::shared_ptr< smartSingleton > p = smartSingleton::getInstance(); // cache instance pointer p->demo(); // Version 2 std::weak_ptr< smartSingleton > pw = smartSingleton::getInstance(); // cache instance pointer pw.lock()->demo(); // Version 3 smartSingleton::getInstance()->demo();
그리고 메모리 릭도 발생하지 않네요. 여러 쓰레드는 서로다은 std::shared_ptr를 통해서 일제히 읽기 쓰기를 수행할 수 있고, 객체가 복사되었을 때에도 객체의 소유 정보를 공유합니다. 하지만 이렇게 DCLP을 이용한 구현에도 여전히 쓰레드 안정성이 보장되지 않습니다.
Version4 - Thread safe singleton C++11
쓰레드 안정성이 보장되는 구현을 위해서는 멀티쓰레드 환경에서 단한번 락을 걸어 생성하는 것을 보장하는 것이 필요합니다.
다행히 C++11에서 우리를 도울 새로운 2가지 요소가 추가되었습니다. std::call_once와 std::once_flag 입니다. 이것을 이용하면 기본 컴파일러는 우리의 Singleton의 쓰레드 안정성과 메모리릭을 보장해 주게 됩니다.
std::once_flag의 인스턴스는 std::call_once와 함께 사용되며 특정 함수가 딱 한번 호출되는 것을 보장해 줍니다 .심지어 여러 쓰레드가 동시에 호출하더라도 한번만 호출합니다.
std::once_flag는 복사생성자, 할당연산자, 이동생성자, 이동연산자를 지원하지 않습니다.
이것이 C++11에서 쓰레드 안정성을 보장하는 구현에 대한 제안입니다.
class safeSingleton { static std::shared_ptr< safeSingleton > instance_; static std::once_flag only_one; safeSingleton(int id) { std::cout << "safeSingleton::Singleton()" << id << std::endl; } safeSingleton(const safeSingleton& rs) { instance_ = rs.instance_; } safeSingleton& operator = (const safeSingleton& rs) { if (this != &rs) { instance_ = rs.instance_; } return *this; } public: ~safeSingleton() { std::cout << "Singleton::~Singleton" << std::endl; } static safeSingleton & getInstance( int id ) { std::call_once( safeSingleton::only_one, [] (int idx) { safeSingleton::instance_.reset( new safeSingleton(idx) ); std::cout << "safeSingleton::create_singleton_() | thread id " + idx << std::endl; } , id ); return *safeSingleton::instance_; } void demo(int id) { std::cout << "demo stuff from thread id " << id << std::endl; } }; std::once_flag safeSingleton::only_one; std::shared_ptr< safeSingleton > safeSingleton::instance_ = nullptr;
getInstance()함수에 전달되는 파라메터는 테스트를 위한 코드일 뿐이니 무시해 주세요. 보시는것 처럼 일반 메서드 대신 람다식을 사용했습니다. 아래는 safeSingleton 클래스를 테스트해본 코드 입니다.
std::vector< std::thread > v; int num = 20; for( int n = 0; n < num; ++n ) { v.push_back( std::thread( []( int id ) { safeSingleton::getInstance( id ).demo( id ); } , n ) ); } std::for_each( v.begin(), v.end(), std::mem_fn( &std::thread::join ) ); // Version 1 std::shared_ptr<smartSingleton> p = smartSingleton::getInstance(1); // cache instance pointer p->demo("demo 1"); // Version 2 std::weak_ptr<smartSingleton> pw = smartSingleton::getInstance(2); // cache instance pointer pw.lock()->demo(2); // Version 3 smartSingleton::getInstance(3)->demo(3);
20개의 쓰레드를 만들고 동시에 getInstance() 함수에 접근하였습니다. 딱 하나의 쓰레드에서만 instance를 생성하는데 성공하였습니다.
http://milennium9.godohosting.com/doku.php?id=cpp:tiptricks:multithread_singleton
'CPP' 카테고리의 다른 글
STL 정렬 (0) | 2015.12.23 |
---|---|
STL 기본 (0) | 2015.12.23 |
모듈 이 safeseh 이미지 에 대해 안전 하지 않습니다 (0) | 2014.10.01 |
싱글톤 패턴 (Singleton Pattern) (1) | 2014.09.29 |