본문 바로가기

진리는어디에

Singleton in Multi Thread

들어가며

이번 포스트는 멀티 스레드 환경에서 싱글톤을 사용할 때 흔히 할 수 있는 실수 한가지에 대해서 살펴 보고자한다. 설명을 위해 한가지 상황을 만들어보자.

우리는 지금 부터 싱글톤 이벤트 큐를 만들어야 한다고 가정. 각종 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을 걸어 싱글톤 객체의 생성자가 완료 될 때까지 스레드들이 진입하지 못하게 하던지, 싱글톤 객체 생성 완료 이후 스레드를 초기화 해주더지 해야 한다.

교훈

싱글톤 생성자에서 스레드를 생성하고, 그 스레드로 다시 싱글톤을 참조하는 바보 같은 짓은 하지 말자.

부록 1. 같이 읽으면 좋은 글

유익한 글이었다면 공감(❤) 버튼 꾹!! 추가 문의 사항은 댓글로!!