singleton




シングルトンとは、アプリケーション全体で、唯一のオブジェクトを生成するパターンを意味します。「シングルd」などと書くと、ちょっと格好いいような、悪いような(笑)

それはともかく。

シングルトンなオブジェクトの生成は、単純には次のように実装します。

list-1
template <class T>
struct singleton {
    T* GetObj() { 
        if (pObj_ == NULL) {
            pObj_ = new T;
        }
        return pObj_;
    }
    singleton() : pObj_(0) {}
    ~singleton() { delete pObj_; }

    T* pObj_;
};


このsingleton::GetObjはstaticな関数からも参照され得るので、このsingleton自体がstaticなメンバとして用意する必要があります。すなわち、使うときは、

list-2
struct CHoge {
    static CHoge* GetObj() { return m_vHoge.GetObj(); }
private:
    static singlton<CHoge> m_vHoge;
};


となります。

ところで、このCHoge::GetObjを、staticなオブジェクトのコンストラクタから呼び出すこともあるのですが、そうすると、singleton::pObj_がそのコンストラクタでNULLに初期化される前に、呼び出されることになります。(staticなオブジェクトの初期化順序は不定のため)

そこで、ひとひねりして、singletonを次のように実装すると良いです。

list-3
    static T*   pObj_;
    //  staticで用意することで、
    //  リンク時(非ローカルなstaticオブジェクトの初期化前)にNULL
    //  であることを保証する
};

//  static なオブジェクト
template <class T> T* singleton<T>::pObj_ = 0;


これで、singletonの問題は、半分は解決なのですが、マルチスレッド時は、この実装ではまずいのです。

マルチスレッド時、この実装では、

枠-1
    T* GetObj() { 
        if (pObj_ == NULL) {
            pObj_ = new T;
        }
        return pObj_;
    }


pObj_がNULLかチェックして、NULLだったとして、次のnew Tを実行しようとした瞬間にスレッドが切り替わり、そのスレッドが、GetObjを行ない、new Tを行なってしまうかも知れません。すなわち、2重に生成してしまうことになるのです。

ちなみに、Effective C++で書かれている、

list-4
    CHoge* GetObj() {
        static CHoge hoge;
        return &hoge;
    }


というような実装の仕方の場合も同じ問題により、マルチスレッド時にはオブジェクトのリークが起こります。

そこで、これを防止するために、

枠-2
    T* GetObj() { 
        同期オブジェクト.lock();
        if (pObj_ == NULL) {
            pObj_ = new T;
        }
        同期オブジェクト.unlock();
        return pObj_;
    }


というように同期オブジェクト(例:CriticalSection)等を導入しても良いのですが、非NULLの場合も同期オブジェクトを獲得しているのが、かなりダサい(負荷のかかる)プログラムだと言えます。

そこで、マルチスレッド時のsingletonパターンは、次のように実装するのが常識です。

枠-3
    T* GetObj() { 
        if (pObj_ == NULL) {
            同期オブジェクト.lock();
            if (pObj_ == NULL){
                pObj_ = new T;
            }
            同期オブジェクト.unlock();
        }
        return pObj_;
    }


すなわち、非NULLのときは、同期オブジェクトを用いずに、そのままpObj_をリターンします。NULLだった場合には、同期オブジェクトを獲得して、そのあと、再度、NULLチェックを行ない(同期オブジェクトを獲得している間に他スレッドが、new Tしている可能性があるので)、そののちにnew Tします。

見てのとおり、2回チェックすることから、「double checked lockingパターン」と呼ばれます。(参考⇒ジョン・ブリシデス著「パターンハッチング」)

ちなみに、コンパイラによっては、同期オブジェクトをlockしている間に、次のNULLチェックを別パイプで行なってしまうコードを生成しかねないので、その場合は、NULLチェックを3回行なう、triple checked lockingパターンを用いなければならないこともあります。

VC++6/7に限って言えば、同期オブジェクトをlockする部分に、::EnterCriticalSectionが用いるのなら、その呼び出しから制御が戻るまで次のif文は実行されないことが保証されるので、double checked lockingで良いです。

それは、ともかく、この同期オブジェクトをどこに配置するのか、という問題があります。

「staticなオブジェクトのコンストラクタ」からsingleton::GetObjは呼び出され得るので、この同期オブジェクトもstaticに用意されなくてはならないのですが、「staticなオブジェクトのコンストラクタ」の生成に先駆けて、この同期オブジェクトが初期化(::InitializeCriticalSection)されていなければなりません。

これは、かなりの難問です。(わたしは、自分の著書のなかで、この部分の解説がおざなりだったので、ここで捕捉します)

staticなオブジェクトのコンストラクタが起動している段階では、まだ他のスレッドは生成されていない、すなわち、マルチスレッドではないことから、同期オブジェクトのlock〜unlockは不要と言えます。そこで、CAppInitializerのコンストラクタが起動するまで(このクラスは、WinMainの先頭に書くことを義務づける)は、同期オブジェクトのlock〜unlockは行なわない、という実装にしてあります。

すなわち、singleton::CheckInstanceは、次のようになっています。

list-5
    void CheckInstance(){
        if (m_lpObj==NULL) {
            if (CAppInitializer::IsMultiThread()){
            //  マルチスレッド時は、double-checked locking
                CCriticalLock guard(&m_cs);
                if (m_lpObj==NULL){
                    m_lpObj = new T;
                }
            } else {
                m_lpObj = new T;
            }
        }
    }


察しの良い方はお気づきになられたかも知れません。
同様に、singleton::Releaseにも、double-checked lockingを導入しなければなりません。またこれに伴い、CAppInitializer::IsMultiThreadは、CAppInitializerのデストラクトタイミングでfalseを返すようにしておきます。(このタイミングでスレッドマネージャに全スレッド停止の要求を出すため、ここ以降はシングルスレッドとして実行されることが保証されるので)

list-6
template <class T> void singleton<T>::Release() {
    if (m_lpObj!=NULL) {
        if (CAppInitializer::IsMultiThread()){
        //  マルチスレッド時は、double-checked locking
            CCriticalLock guard(&m_cs);
            if (m_lpObj!=NULL){
                delete m_lpObj;
                m_lpObj = NULL;
            }
        } else {
            delete m_lpObj;
            m_lpObj = NULL;
        }
    }
}


ごちゃごちゃ書いてしまいましたが、使いかたは、いたって簡単。さきほどの例のように、

list-7
struct CHoge {
    static CHoge* GetObj() { return m_vHoge.get(); }
private:
    static singlton<CHoge> m_vHoge;
};


と書けば、CHoge::GetObj() によって、シングルトンなオブジェクトを取得できます。yaneSDK3rdのクラスのなかに、シングルトンで用いるのが前提となるクラスがあって、そういうクラスでは、上記のようにGetObjメソッドが用意してあるので、これを用いて、シングルトンなオブジェクトを取得して、そいつ間接でアクセスします。