싱글톤 패턴 (Singleton Pattern) [두번째]

Singleton(싱글톤) 패턴 첫번째 강의에서 구현했던 Singleton class 는 경쟁이 심한 멀티스레드 환경에서 약점이 있다고 말씀드렸습니다.

앞서 살펴보았던 puppy_class 를 다시 한번 볼까요 ?

class puppy_class
{
private:
    static puppy_class *instance;  /* 객체를 가리키는 static 포인터 */
public:
    puppy_class(int arg)  { printf("puppy_no : %d\n", arg); }
    static puppy_class *get_instance(int arg)
    {
        if(instance == NULL)
        {
            /* 객체가 아직 생성되지 않았을 경우에만 객체 생성 */
            instance = new puppy_class(arg);
        }
        return instance;
    }
};

위의 코드에서 if(instance == NULL) 은 현재 객체가 이미 만들어져 있는지를 검사하기 위한 것입니다.
NULL 인 경우에만 새로운 객체를 생성하여 instance 멤버포인터 변수에 해당 객체의 주소값을 저장해둘 것입니다.

그런데, 만약 1000 개의 스레드가 거의 동시에 if(instance == NULL) 코드 영역으로 들어왔다고 생각해보죠.
어떠한 현상이 벌어질까요 ?

첫번째 스레드가 instance 가 NULL 임을 확인한 후 새로운 객체를 생성하기 직전에, 공교롭게도 다른 스레드 또한 instance 가 NULL 인 것으로 판단할 수 있을 것입니다.
스레드들이 같은 코드를 거의 동시에 수행할 수 있는 가능성이 있는 로직에서는 얼마든지 벌어질 수 있는 상황입니다.

그렇게 되면 아마도 우리가 의도치 않게 객체가 둘 이상이 생성될 수 있습니다.
그런 상황에서 첫번째 생성된 객체에 대한 포인터는 그 이후에 생성된 객체 포인터에 의해 교체될 것입니다.
이후의 상황이 장애로 이어질지 아닐지는 아마도 해당 class 가 어떤 변수를 가졌는지와 함수들이 어떠한 일을 수행하는지에 따라 다를 것입니다.

만약 첫번째 스레드가 자신이 생성한 객체 포인터를 이용하여 obj->some_member_func()와 같이 특정 멤버함수를 호출했는데, 해당 함수 내에서 자신의 멤버 변수를 수정하는 작업을 수행하려던 찰나, 다른 스레드가 새로운 객체를 생성하여 static instance 변수에 포인터를 대입해버렸다면 ?
obj 는 자신이 생성한 객체가 아니고 다른 스레드가 생성한 객체를 가리키게 될 것입니다.
이러한 상황은 장애 발생 여부를 떠나서 일단 의도하지 않은 상황이기 때문에 회피하는 것이 좋습니다.

그러면, 이러한 미묘한 스레드 간의 동시성 제어를 어떻게 수행하는 것이 효과적일까요 ?

결국 해법은 스레드가 위의 문제가 되었던 코드 영역에 한꺼번에 몰려들지 않고 줄을 서게 만드는 것입니다. Lock 을 이용하거나 semaphore, spinlock 등 이를 해결하는 방법은 다양할 수 있습니다.
Java 에서는 syncronized 라는 예약어를 사용하여 스레드들을 동기화시킵니다.
C++ 에서는 이 예약어를 지원하지 않기 때문에 사용자가 직접 구현해야 합니다.

아마도 개념적으로 다음과 같이 구현해야할 것입니다.

class puppy_class
{
private:
    static pthread_mutex_t  mutex;  /* mutex 변수 */
    static puppy_class *instance;
public:
    puppy_class(int arg)  { printf("puppy_no : %d\n", arg); }
    static puppy_class *get_instance(int arg)
    {
        /* instance 가 NULL 인 것을 검사하기 전에 lock 을 걸어 스레드들을 줄세움 */
        pthread_mutex_lock(&mutex);
        if(instance == NULL)
        {
            instance = new puppy_class(arg);
        }
        pthread_mutex_unlock(&mutex);
        return instance;
    }
};

위의 예제는 mutex lock 을 이용하여 get_instance 함수내에서 스레드들을 줄세우는 방법입니다.
위와 같은 상황에서는 mutex_lock 영역을 하나의 스레드만 진입할 수 있으므로 첫번째 스레드가 객체를 생성한 후 unlock 으로 빠져나가면, 나머지 스레드들은 정확하게 instance != NULL 이게 될 것이므로 추가적인 객체를 생성할 일은 없을 것입니다.

위처럼 mutex lock 을 사용해도 되지만, 사용자는 성능 등을 고려하여 atomic operation 을 이용하여 spinlock 을 직접 구현해서 같은 목적을 달성해도 됩니다. 방법은 다양하겠죠. ^^

그렇다면 위의 코드는 문제가 전혀 없을까요 ?
다음 번에는 또 다른 상황에서 발생할 수 있는 문제를 살펴보도록 하겠습니다.

You may also like...

0 0 votes
Article Rating
Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x