싱글톤(Singleton) 은 인스턴스를 생성하는 몇가지 패턴중 하나이다.
프로그램에서 오직 하나의 인스턴스만을 생성할 수 있도록 보장하며 생성된 인스턴스를 전역에서 참조 할 수 있도록 정적 메서드를 제공하는데 보통 getInstance() 또는 instance() 와 같은 이름으로 구현한다.
프로그램에서 오직 하나의 인스턴스만 생성하도록 강제한다는 의미는 객체에서 메모리 할당과 해제등을 제어한다는 것이며 시스템에서 하나의 매니저 역할을 수행하는 객체를 구현할때 유용하게 사용된다.
싱글톤 객체는 위에 설명했듯이 정적 메서드를 통해 제공된다. 아래 예제를 살펴보자.
class MySingleton{ public: static MySingleton& getInstance(); private: MySingleton()= default; ~MySingleton()= default; MySingleton(const MySingleton&)= delete; MySingleton& operator=(const MySingleton&)= delete; };
위의 예제에서 getInstance() 메서스는 static로 선언되어 클라이언트는 오직 이 메스드를 통해서만 이 객체를 참조할 수 있게 된다. 포인터가 아닌 참조를 반환한 것은 클라이언트가 잠재적으로 객체를 제거할 여지가 있기 때문이다.
또한 완전한 싱글톤 객체를 유지하기 위해서 기본 생성자 및 복사생성자, 소멸자등을 private로 선언하였다.
이제 이 클래스에서 제공하는 API를 사용하는 클라이언트는 아래와 같은 방법으로 인스턴스를 참조한다.
MySingleton &myInstance = MySingleton::getInstance();
getInstance() 구현
getInstance()를 구현하는 방법은 여러가지가 있는데 그중 C++11에서 가장 일반적이며 또한 심플한 방법으로
Meyers Singeton으로 구현 할 수 있다. 또한 이것은 다중 쓰레드에서 안전한 방법이기도하다.
//Meyers Singleton MySingleton &MySingleton::getInstance(){ static MySingleton instance; return instance; }
Meyers는 local 정적 변수가 아닌 정적 변수 초기화의 순서는 정해져 있지 않다고 하였는데 즉, 전역 변수로 사용해서 싱글톤 클래스를 초기화 하는 것은 위험하다는 의미이다. 그래서 getInstance()메서드 안에서 정적 변수를 생성하였다.
이제 getInstance()가 처음으로 불려질때 정확히 하나의 객체만 생성된다.
아래의 예제는 여러개의 쓰레드에서 getInstance()를 호출하는 것이다.
#include <iostream> #include <future> class MySingleton{ public: static MySingleton& getInstance(){ static MySingleton instance; return instance; } private: MySingleton()= default; ~MySingleton()= default; MySingleton(const MySingleton&)= delete; MySingleton& operator=(const MySingleton&)= delete; }; std::chrono::duration<double> getTime(size_t tenMill){ auto begin= std::chrono::system_clock::now(); for ( size_t i= 0; i <= tenMill; ++i){ MySingleton::getInstance(); } return std::chrono::system_clock::now() - begin; } int main(){ auto fut1 = std::async(std::launch::async, getTime, 100000); auto fut2 = std::async(std::launch::async, getTime, 10000000); auto fut3 = std::async(std::launch::async, getTime, 5000000); std::cout << "fur1: " << fut1.get().count() << std::endl; std::cout << "fur2: " << fut2.get().count() << std::endl; std::cout << "fur3: " << fut3.get().count() << std::endl; }
결과:
쓰레드에 안전한 싱글톤 구현의 다른 방법으로 std::call_once 함수와 std::once_flag 플래그를 이용한 것이다.
std::call_once 함수를 사용하여 정확히 한번 실행 가능한 목록에 등록할 수 있으며 std::once_flag 플래그는 단일 객체가안전하게 초기화됨을 보장한다. 아래는 예제 소스코드이다.
#include <iostream> #include <future> class MySingleton{ public: static MySingleton& getInstance(){ std::call_once(initInstanceFlag, &MySingleton::initSingleton); return *instance; } private: MySingleton()= default; ~MySingleton()= default; MySingleton(const MySingleton&)= delete; MySingleton& operator=(const MySingleton&)= delete; static MySingleton* instance; static std::once_flag initInstanceFlag; static void initSingleton(){ instance= new MySingleton; } }; MySingleton* MySingleton::instance = nullptr; std::once_flag MySingleton::initInstanceFlag; std::chrono::duration<double> getTime(size_t tenMill){ auto begin= std::chrono::system_clock::now(); for ( size_t i= 0; i <= tenMill; ++i){ MySingleton::getInstance(); } return std::chrono::system_clock::now() - begin; } int main(){ auto fut1 = std::async(std::launch::async, getTime, 100000); auto fut2 = std::async(std::launch::async, getTime, 10000000); auto fut3 = std::async(std::launch::async, getTime, 5000000); std::cout << "fur1: " << fut1.get().count() << std::endl; std::cout << "fur2: " << fut2.get().count() << std::endl; std::cout << "fur3: " << fut3.get().count() << std::endl; }
결과:
이밖에 쓰레드 동기화를 위한 뮤텍스(mutex) 잠금으로 구현하기도 한다. 다만 이방법은 getInstance()가 호출 될때마다 잠금이 발생함으로 성능과 관련한 비용이 든다는 단점이 있다.
위의 예제를 통해서도 성능차이를 짐작할 수 있었다. 물론 위의 코드는 최적화 되지 않았으며 실행되는 시스템 환경등의 차이는 감안해야 할것이므로 참고만 하자.
참고:
http://www.modernescpp.com/index.php/thread-safe-initialization-of-a-singleton