들어가며
이번 포스트는 멀티 스레드 환경에서 싱글톤을 사용할 때 흔히 할 수 있는 실수 한가지에 대해서 살펴 보고자한다. 설명을 위해 한가지 상황을 만들어보자.
우리는 지금 부터 싱글톤 이벤트 큐를 만들어야 한다고 가정. 각종 read/write 작업들을 '싱글톤' 이벤트 큐에 집어 넣고, 몇 개의 '스레드'들이 큐를 감시하다, 큐에 새로운 이벤트가 들어 오면 이벤트에 따라 적적한 작업을 해주는 방식인 전형적인 producer/consumer 방식이다. 이 상황에서 중요한것은 '싱글톤'과 몇개의 '스레드'들이다. 그리고 스레드를 만들게 되면 의례 그렇듯이 아래와 같이 레이스 컨디션이 발생하게 된다.
시간 | ThreadA | ThreadB |
1 | if(queue.empty()) -> not empty | |
2 | if(queue.empty())-> not empty too | |
3 | queue.pop() | |
4 | queue.pop() -> 큐는 이미 비었다 Error!! |
위와 같은 문제 때문에 Thread 가 큐에 이벤트를 집어 넣거나(enque) 빼는(pop) 작업을 할 때는 동기화를 걸어 주어야 한다.
구현
싱글톤 이벤트 큐는 간단하게 아래와 같이 구현 할 수 있을 것이다.
- getInstance() 함수로 EventQueue의 생성자가 호출 되면, 생성자 내부에서 WorkerThread들을 호출 하고
- WorkerThread들은 EventQueue에 이벤트가 들어 왔는지를 루프를 돌면서 확인한다(지면상 간단히 상상이 가능한 부분은 생략 하도록 한다. 별로 중요한 부분은 아니니 대충 보고 넘어가도 된다.)
queue
class EventQueue {
public :
virtual ~EventQueue();
/**
Singleton 객체 생성
*/
static EventQueue* getInstance() {
if(m_selfInstance == NULL) {
m_selfInstance = new EventQueue();
}
return m_selfInstance;
}
bool push(EVENT event); // 구현 생략
EVENT pop(); // 구현 생략
bool empty(); // 구현 생략, 비었으면 true, 그렇지 않으면 false
private :
EventQueue() {
pthread_mutex_init(&m_mutex, NULL);
for(int i=0; i<WORKERTHREAD_COUNT; i++) {
m_thread[i].init(&m_mutex); // thread에게 뮤텍스의 포인터를 넘겨 준다.
m_thread[i].start(); // create_thread 류의 함수를 호출하여 thread를 동작 시킨다.
}
}
typedef std::queue<EVENT> EVENT_QUEUE;
static EventQueue* m_selfInstance;
EventWorkerThread m_thread[WORKERTHREAD_COUNT];
pthread_mutex_t m_mutex;
};
thread
void EventWorkerThread::init(pthread_mutex_t* mutex) {
m_mutex = mutex; //threa의 멤버 변수에 EventQueue의 mutex 변수 포인터를 넘겨 준다.
// 이로써 mutux를 공유 할 수 있다.
}
/**
실제 thread가 작업을 하는 부분이다.
create_thread 라던지 그런것은 다 접어 두고, 그냥 thread가 이런 작업을 하게끔 되어 있구나 라고 생각하자.
*/
void* EventWorkerThread::run() {
while(1) {
/* critical section.. 동기화가 제대로 된다면 1 : ...., 2 : .... 의 형식으로 나와야 한다.*/
pthread_mutex_lock(m_mutex); // EventQueue로 부터 넘겨 받아온 mutex 포인터다
std::cout << "1 : " << (int)this << ":" << (int)m_mutex << std::endl;
if(!EventQueue::getInstance()->empty()) {
m_event = EventQueue::getInstance()->pop();
}
std::cout << "2 : " << (int)this << ":" << (int)m_mutex << std::endl;
pthread_mutex_unlock(m_mutex);
if(m_event.type & EVENT_WRITE) {
// 이벤트가 발생 하면 뭔가를 하긴 한다.
}
}
}
문제
동기화가 전혀 안되고 있다!!!
원인
Singleton과 그 singleton 객체를 참조하는 Thread가 문제다!! 왜 그런지 아래 시나리오를 따라가며 보도록 하자. 지금 부터 매우 중요하다. 집중하자.
1. 어딘가에서 EventQueue에 뭔가를 넣기 위해 singleton 객체를 호출 한다고 하자.
static EventQueue* getInstance() {
if(m_selfInstance == NULL) {
m_selfInstance = new EventQueue(); // 현재 여기까지 진행 되었음
}
return m_selfInstance;
}
- 3 라인 : 프로그램이 3라인 까지 진행 되었다고 가정하자.
"if(m_selfInstance == NULL)"를 통과 했으므로, 싱글톤 객체가 생성 되지 않은 상태다. EventQueue() 생성 자가 호출 된다.
2. 쓰레드 생성
EventQueue() {
pthread_mutex_init(&m_mutex, NULL);
for(int i=0; i<WORKERTHREAD_COUNT; i++) {
m_thread[i].init(&m_mutex); // thread에게 뮤텍스의 포인터를 넘겨 준다.
m_thread[i].start(); // 이때 thread가 구동된다.
}
}
- 5 라인 : thread 객체의 start()를 호출하면 스레드가 시작된다. 스레드가 시작 되면 위 EventWorkerThread::run() 가 바로 시작 된다고 가정하자.
3. WokerThread가 EventQueue 감시
pthread_mutex_lock(m_mutex); // EventQueue로 부터 넘겨 받아온 mutex 포인터다
std::cout << "1 : " << (int)this << ":" << (int)m_mutex << std::endl;
if(!EventQueue::getInstance()->empty()) {
m_event = EventQueue::getInstance()->pop();
}
std::cout << "2 : " << (int)this << ":" << (int)m_mutex << std::endl;
pthread_mutex_unlock(m_mutex);
- 3 ~ 4 라인 : 스레드는 내부에서 getInstance()를 호출 하고 있다. 아직 EventQueue의 생성자는 다 완료 되지 않았고 m_selfInstance 변수에 싱글톤 객체를 할당 하지 못했을 수도 있다. 이럴 경우 EventQueue의 또 다른 Singleton 객체가 만들어진다. 그리고 그에 따라 뮤텍스 객체도 추가로 생성되어 각 스레드마다 각자의 싱글톤 객체와 각자의 뮤텍스를 바라보게 될 수도 있다.
해결
생성자에 lock을 걸어 싱글톤 객체의 생성자가 완료 될 때까지 스레드들이 진입하지 못하게 하던지, 싱글톤 객체 생성 완료 이후 스레드를 초기화 해주더지 해야 한다.
교훈
싱글톤 생성자에서 스레드를 생성하고, 그 스레드로 다시 싱글톤을 참조하는 바보 같은 짓은 하지 말자.