C++
에서 싱글톤
을 구현할때 항상 따라오는 문제는 멀티스레딩의 문제이다.
하지만 대부분이 이 부분을 잘못 이해하고 있는것 같다.
싱글톤 포스팅한 블로그를 하나하나 보자니 하나같이 ctrl-c, v
를 했는지, 말도안되는 이야기만 잔뜩 써놓았더라.
싱글톤을 크게 일반 싱글톤
, 다이나믹 싱글톤
으로 나누는것을 볼 수 있는데
다이나믹 싱글톤
에서 인스턴스가 두번 생성될수 있는 문제를 Double Checked locking
같은 방법으로 처리한다. 하지만 이는 멀티스레딩 문제의 시작일 뿐이다.
하지만 대부분의 어플리케이션의 경우 싱글톤에 사용되는 객체의 크기가 그렇게 크지 않으며,
다이나믹 싱글톤
으로 구현할때 따라오는 생성자/소멸자의 문제는 더욱 골치아파진다.
본격적으로 이를 까보겠다.
먼저 다이나믹 싱글톤
의 구현에서 이중체크 방식은 확실히 불안정한 요소가 존재한다.
test1.cpp
#include<iostream>
#include<thread>
using namespace std;
class AAA {
static int* inst;
public:
static int* get_inst() {
if (AAA::inst == nullptr) {
cout << "new" << endl;
AAA::inst = new int;
}
return AAA::inst;
}
};
int* AAA::inst = nullptr;
int main() {
auto f= []()->void {
*AAA::get_inst() = 5;
int a = *AAA::get_inst();
};
thread a(f);
thread b(f);
thread c(f);
thread d(f);
a.join();
b.join();
c.join();
d.join();
return 0;
}
g++ test1.cpp -std=c++11 -pthread
haneul@mint ~/문서/SingleTon $ ./a.out
newnew
new
new
haneul@mint ~/문서/SingleTon $ ./a.out
new
new
new
new
haneul@mint ~/문서/SingleTon $ ./a.out
new
new
new
haneul@mint ~/문서/SingleTon $ ./a.out
new
결과는 참으로 암담하다. 마지막 실행만 우리가 원하는대로 실행되었다.
이유는 간단하다. 스레드들이 if문을 통과하고 스케줄링이 바뀌어서 다른 스레드역시 if문을 통과했기 때문이다.
그렇다면 Double Checked locking
은 if
문을 두번 잠그면 되나? 아니다.
저런거 몇번을 잠궈도 소용없다.
바로 두번째 if
문을 mutex
로 감싸는 방법이다.
static int* get_inst() {
if (inst == nullptr) {
AAA::mtx.lock(); //begin locking area
if (inst == nullptr) {
cout << "new" << endl;
inst = new int;
}
AAA::mtx.unlock(); //end locking area
}
return inst;
}
실행결과는 직접 눈으로 보길 바란다.
mutex
를 쓰던 다른 매커니즘을 쓰던 상관 없다.
이제 다이나믹 싱글톤
생성에 관한 문제는 끝이 났다.
문제는 소멸자이다. 동적할당한 싱글톤은 해제하고싶으면 해제메소드를 만들면되고,
atexit
같은 쓸데없는거에 등록할 필요없다. 어차피 프로그램이 종료되면 알아서 OS가 해제해준다.
물론 아니지. get_inst()
를 호출할때마다, 파이프라이닝이 생긴다.
(처음부터 lock 시키는거보단 파이프라이닝이 성능저하가 덜함, 그래서 if문을 두번 쓰는것임)
하지만 처음부터 메모리를 쓰는게 불만이라면 다이나믹 싱글톤
을 사용해야한다.
당연히 생성자가 있는 클래스를 지역 static
으로 선언하고 해당 함수를 호출하지 않는다면, 생성자가 호출되지 않겠지.
근데 생성자가 호출되지 않는거랑, 메모리를 먹고있는거는 완전히 다른 개념이다.
어차피 DATA
영역에 생성되는 static
변수인데, 이게 쓰이던 안쓰이던 static
으로 선언되면
해당 메모리 영역을 컴파일 타임부터 먹고있는것이다.
멀티스레딩을 고려한 싱글톤은 생성될때 이중라킹을 건다는걸 의미하는게 아니다.
저거 하나 걸면 뭐하나, 멀티스레딩에서 R/W 작업시 오류가 나는걸...
get_inst()
에서 가져온 변수에 쓰기 작업을 할때 변수가 깨진다.
멀티스레딩에 대해 좀 알아보자.
우선 Critical Section
은 데이터에 Lock이 걸리는것이 아니다. 단순히 접근을 하지말라는 경고를 한것 뿐이다.
(mutex는 내부적으로 Critical Section 을 사용한다.)
데이터를 읽을때는 스레드 동기화가 필요없다고 생각하는 사람이 많은데, 만일 쓰기작업이 진행중인데, 읽기가 진행된다면 문제가 발생한다.
따라서 쓰기 작업시에는 다른 스레드의 (읽기/쓰기) 가 모두 정지되어야 한다.
읽기 작업시에는 위에서 말했던 이중 확인 생성, 그리고 쓰기작업이 진행중인지 확인하면 된다.
C++11
에는 lock_guard
라는 클래스가 있다. 이는 생성될때 복사생성자로 받은 뮤텍스를 lock 시키고 해당 객체가 소멸될때 뮤텍스를 unlock 시킨다.
static int& SingleTon::read(){
/*Double Checked Locking*/
lock_guard<mutex> guard(SingleTon<T>::mtx);
return *SingleTon::instance;
}
대략적으로 위와같이 사용하면된다.
http://stackoverflow.com/questions/3856729/how-to-use-lock-guard-when-returning-protected-data
위의 링크에 따르면 lock_guard
는 리턴된 변수의 읽기 까지 스레드 안정성을 보장한다고 되어있다.
쓰기 작업은 쓰기작업 전체를 lock/unlock
으로 감싸야 하기 때문에
보통의 경우 싱글톤 객체를 받는 Set()
이라는 메소드를 제공하게 구현하는데,
해당 방식은 클래스를 통채로 받기 때문에, 차라리 람다를 받게 구현하는것이 더 편리할것 같다.
template<typename F>void SingleTon::write(F func) {
SingleTon<T>::mtx.lock();
if (SingleTon<T>::instance == NULL) {
SingleTon<T>::instance = new T;
}
func(*SingleTon<T>::instance);
SingleTon<T>::mtx.unlock();
}
read/write 둘중 뭐가 먼저 될지 모르니 둘다 생성 코드를 넣도록 하자.
이를 라이브러리화 한다면 라이브러리의 공식
1. 라이브러리는 잘못쓰기 어려워야한다.
2. 사용하기 편리해야 한다.
이 두가지 조건을 갖추게 template
으로 구현하고, 해당 템플릿 인수가되는 클래스는
대입,주소연산등 어떠한 경우에도 read()
메소드를 통해 데이터를 취득하여 write()
가아닌 다른곳에서 데이터를 쓸수 없게 해야한다.
따라서 복사생성자,대입연산자,주소연산자 를 모두 막은 클래스를 상속받은 클래스만,
싱글톤에서 사용이 가능해야 한다.
이러한 싱글톤 라이브러리는
https://github.com/springkim/Design-Pattern/tree/master/C%2B%2B/SingleTon
에서 확인할 수 있으며, 사용법 또한 위 링크에 나와있다.
멀티스레딩에 관한 내용은 아래의 블로그에서 괜찮게 설명해놓은것 같다.
http://kuaaan.tistory.com/116
http://stackoverflow.com/questions/27860685/how-to-make-a-multiple-read-single-write-lock-from-more-basic-synchronization-pr
싱글톤의 Double Checked Locking은 아래의 문서에서 확인할 수 있다.
http://preshing.com/20130930/double-checked-locking-is-fixed-in-cpp11/
http://stackoverflow.com/questions/12248747/singleton-with-multithreads