2009년 12월 9일 수요일

singleton 이야기 I.

// header

class singleton

{

public:

    static singleton* instance();

 

private:

    singleton();

    singleton(const singleton&);

    singleton& operator=(const singleton&);

    ~singleton();

    static singleton* instance_;

};

// implementation

singleton* singleton::instance_ = NULL;

 

singleton* singleton::instance()

{

    if( instance_ == NULL )

    {

         instance_ = new singleton;

    }

    return instance_;

}

 

singleton을 구현한다면, 아마도 위와 같은 idiom을 따를 것이다①. 그리고 이미 잘 알려진대로 멀티쓰레드 환경에서 이 구현은 문제점을 가지고 있다.

 

singleton* singleton::instance()

{

    if( instance_ == NULL )          

    {                                          <- shit!

         instance_ = new singleton;

    }

    return instance_;

}

 

만약 A와 B 쓰레드가 있는데 A쓰레드가 instance함수에 진입해서 <-shit!이라고 표시된 곳에서 suspend된다면, B 쓰레드가 instance()함수에 진입해서 instance_가 아직 NULL이기때문에 객체를 초기화하고 그 주소를 반환할 것이다. 이후 A가 resume되서 <-shit이후 구문을 실행하기 때문에 당연히 또 객체를 초기화하고 그 주소를 반환할 것이다.

 

인스턴스가 두 개나 생기다니, 뭐가 singleton이란 말인가? 가상의 인물 tk군은 싱글쓰레드 환경에서 잘 작동하던 singleton 클래스를 멀티쓰레드 환경에서도 잘 작동하도록 하기 위해서 instance함수에 lock객체를 추가한다.

 

singleton* singleton::instance()

{

    lock guard;

    if( instance_ == NULL )          

    {

         instance_ = new singleton;

    }

    return instance_;

}

 

휴, 이제 하나의 인스턴스만 생긴다. 하지만 instance_가 한번 초기화되고나면 바로 그 주소를 반환하면되는데, instance_가 초기화된 이후에도 어떤 쓰레드가 이미 선점하고 있으면 대기를 해야하는 상황이니, 성능저하가 이만저만이 아니다.

 

그래서 tk군은 instance_객체를 초기화할 때만 보호가 필요하므로, lock객체의 생성 위치를 변경하면 문제가 해결될 것이라 생각하고 lock객체의 생성 위치를 바꾼다.

 

singleton* singleton::instance()

{

    if( instance_ == NULL )          

    {

         lock guard;                       <- in series

         instance_ = new singleton;

    }

    return instance_;

}

 

그런데 만약 A쓰레드가 lock을 걸고 초기화를 하기전에 B쓰레드가 이미 if문 블럭안으로 들어가서 대기한다면, 처음 구현이랑 똑같은 상황이다. tk군 바보짓했다.

잘생각해보면, lock객체로 인해서 <-in series부터는 순차적인 실행이된다. 그러므로 A,B쓰레드 중 누가되었건 lock을 획득했다면, 그 뒤에 대기하고 있는 쓰레드는 객체가 생성된 이후에 진입이 허용될 것이다. 그러면 그 곳에서 한번 더 instance_가 NULL인지 비교한다면 instance_가 두 번 초기화되지 않을 것이다.

아하! tk군이 위의 생각대로 함수를 다시 작성한다.

 

singleton* singleton::instance()

{

    if( instance_ == NULL )          

    {

         lock guard;                       <- in series

         if( instance_ == NULL )       <- 2nd test

             instance_ = new singleton;

    }

    return instance_;

}

 

A쓰레드가 lock을 하고 2nd test를 하면 당연히 instance_는 NULL이므로 초기화한다. 그 다음 대기하고 있던 B쓰레드가 2nd test를 수행하면 이미 instance_는 NULL이 아니므로 두 번 초기화되지 않는다.

 

휴 문제 해결~~~

 

지금까지 GoF책②에 소개된 singleton pattern이 멀티쓰레드 환경에서 발생하는 문제점때문에 그 해결 방법으로 Douglas C. Schmidt에 의해서 소개된 Double-Checked Locking Pattern(DCLP)③에 대한 이야기였다.

 

다음에는 컴파일러들이 CPU아키텍쳐에 최적화된 코드를 생성하다보니 발생하는 DCLP의 문제점과 간단한 해결 방법에 대한 이야기이다.

 

각주

펼쳐두기..


 

 

댓글 없음:

댓글 쓰기