コンテンツにスキップ

最終更新:2026-10-13

C++ シングルトンパターン

0.はじめに

面接でデザインパターンの話題を避けられない方は多いでしょう。本節では、面接で最もよく聞かれるデザインパターン(シングルトンパターン)について、分類、スレッド安全性、C++11標準に基づかない観点とC++11標準に基づく観点のそれぞれから、スレッドセーフなシングルトンの解決策を説明します。この記事をじっくり読めば、今後の面接で心配する必要はなくなります。

1.よく知られたシングルトン

一般的な書籍やよく知られているシングルトンパターンは次のようなものです:

class singleton {
private:
    singleton() {}
    static singleton *p;
public:
    static singleton *instance();
};

singleton *singleton::p = nullptr;

singleton* singleton::instance() {
    if (p == nullptr)
        p = new singleton();
    return p;
}

これは非常にシンプルな実装です。コンストラクタをprivateまたはprotectedに宣言して外部の関数によるインスタンス化を防ぎ、内部の静的なクラスポインタが唯一のインスタンスを保持し、インスタンスの生成はpublicメソッドで行い、そのメソッドがこのクラスの唯一のインスタンスを返します。

もちろんこのコードはシングルスレッドでしか使えず、マルチスレッドでは安全ではありません。2つのスレッドが同時に初めてinstanceメソッドを呼び出し、同時にpがnullptrであると判定する状況を考えてみましょう。すると2つのスレッドが同時にインスタンスを生成してpに代入し、シングルトンの原則に違反してしまいます。

2.レイジーとイージャー

シングルトンの実装方法は2種類に分かれます:

  • レイジー(遅延初期化)
  • クラスのインスタンスが最初に必要になったときに初めてインスタンス化します。上記はレイジー実装です。
  • イージャー(即時初期化)
  • シングルトンクラスを定義した時点でインスタンス化します。

ここでもイージャーの実装を示します:

class singleton {
private:
    singleton() {}
    static singleton *p;
public:
    static singleton *instance();
};

singleton *singleton::p = new singleton();
singleton* singleton::instance() {
    return p;
}

もちろんこれはスレッドセーフです。普段「スレッドアンセーフ」として語られるのはレイジー方式で、以下ではレイジー方式をスレッドセーフにするコードの最適化を説明します。

3.マルチスレッドでのロック

C++にはRAIIを原理とするロック用のクラスがあり、unlockを手動管理する必要がありません。それがlock_guardです。ここではこれを使ってロックします。

class singleton {
private:
    singleton() {}
    static singleton *p;
    static mutex lock_;
public:
    static singleton *instance();
};

singleton *singleton::p = nullptr;

singleton* singleton::instance() {
    lock_guard<mutex> guard(lock_);
    if (p == nullptr)
        p = new singleton();
    return p;
}

この書き方では、上記のように2つのスレッドがともにp==nullptrの中まで実行してしまう状況は起こりません。スレッドAがp = new Singleton()を実行している間に、スレッドBがinstance()を呼び出した場合、Bは必ずロック取得の場所でブロックされ、スレッドAの実行終了後にロックが解放されるのを待ちます。したがってスレッドセーフです。

しかしこの書き方はパフォーマンスが非常に低いです。instance()を呼び出すたびにロックを取得・解放しますが、この手順が必要なのは最初のnew Singleton()の時だけです。pが生成された後は、いくつのスレッドが同時にアクセスしても、if (p == nullptr)での判定で十分です(ただの読み取り操作で、ロックは不要)。スレッド安全の問題はなく、ロックをかけると逆に性能面の問題が生じます。

そこでDCLが登場します。

4.ダブルチェックロックパターン

上の書き方は状況にかかわらずロックを取得して解放しますが、読み取り操作にはスレッド安全性の問題はないため、必要なのは初回のインスタンス生成時だけで、それ以降は不要です。まずDCLPの実装を見てみましょう:

singleton* singleton::instance() {
    if(p == nullptr) {  // 第一次检查
        Lock lock;
        if(p == nullptr){ // 第二次检查
            p = new singleton;
        }
    }
    return p;
}

上記を踏まえて、ダブルチェックロック+自動回収を書くことができます

class singleton {
private:
    singleton() {}

    static singleton *p;
    static mutex lock_;
public:
    static singleton *instance();

    // 实现一个内嵌垃圾回收类
    class CGarbo
    {
    public:
        ~CGarbo()
        {
            if(singleton::p)
                delete singleton::p;
        }
    };
    static CGarbo Garbo; // 定义一个静态成员变量,程序结束时,系统会自动调用它的析构函数从而释放单例对象
};

singleton *singleton::p = nullptr;
singleton::CGarbo Garbo;
std::mutex singleton::lock_;

singleton* singleton::instance() {
    if (p == nullptr) {
        lock_guard<mutex> guard(lock_);
        if (p == nullptr)
            p = new singleton();
    }
    return p;
}

DCLPの要点は、instanceの呼び出しの大半ではpが非NULLであることが見えるため、初期化を試みる必要すらないことです。そのため、DCLPはロックを取得しようとする前にpがNULLかどうかを確認します。確認が成功した場合(つまりpがまだ初期化されていない場合)にのみロックを取得し、その後pがまだNULLかどうかを再度確認します(だからダブルチェックロックと呼ばれます)。2回目のチェックは必要です。先ほど見たように、別のスレッドが最初のチェックの後、ロックの取得に成功する前に偶然にもpを初期化してしまう可能性が十分にあるからです。

上記のコードは非常によく見えますが、かなりの時間が経ってからようやくこの欠陥が発見されました。原因は:**メモリ読み書きの順序変更(コンパイラの問題)**です。

もう一度pを初期化するあの行を考えてみましょう:

p = new singleton;

この文は3つのことを引き起こします:

  1. singletonオブジェクトを格納できるメモリを割り当てる;
  2. 割り当てられたメモリ上にsingletonオブジェクトを構築する;
  3. pにその割り当てられたメモリを指させる。

この3つのステップが順番に実行されると思うかもしれませんが、実際にはステップ1が最初に実行されることしか保証されておらず、ステップ2と3はそうとは限りません。問題はまさにここにあります。

  • スレッドAがinstanceを呼び出し、pの最初のテストを実行し、ロックを取得し、1と3を実行してから中断されます。この時点でpは非NULLですが、pが指すメモリにはまだSingletonオブジェクトは構築されていません。
  • スレッドBがinstanceを呼び出し、pが非NULLだと判定して、それをinstanceの呼び出し元に返します。呼び出し元はポインタを参照解除してシングルトンを取得しようとしますが、それはまだ構築されていないオブジェクトです。ここでバグが発生します。

DCLPが正しく動作するのは、ステップ1と2がステップ3の前に実行される場合のみですが、CやC++にはこの制約を表現する方法がありません。これはDCLPの心臓に突き刺さった短剣のようなものです。私たちは命令の相対的な順序に制約を課す必要があるのに、言語にはその制約を表現する手段がないのです。

5.memory barrier命令

DCLPの問題はC++11で解決されました。

新しいC++11は新しいメモリモデルを規定しており、上記の3つのステップの実行中にスレッド切り替えが起こらないこと、つまりこの初期化処理が「原子的」な操作であることを保証します。これによりDCLは再び正しく使えます。ただしC++11にはさらに簡潔なマルチスレッドシングルトンの書き方があり、それは後で紹介します。

C++11より前での解決方法はbarrier命令です。正しく実行させるには、ステップ2と3の間にmemory barrierを置き、CPUが1、2、3の順で実行するよう強制します。

第1の実装:

operator new+placement newに基づき、1、2、3の実行順序に従ってコードを書きます。

// method 1 operator new + placement new
singleton *instance() {
    if (p == nullptr) {
        lock_guard<mutex> guard(lock_);
        if (p == nullptr) {
            singleton *tmp = static_cast<singleton *>(operator new(sizeof(singleton)));
            new(tmp)singleton();
            p = tmp;
        }
    }
    return p;
}

第2の実装:

ASMアセンブリ命令mfenceを直接埋め込む方法に基づきます。Unixのbarrierマクロもこの命令によって実装されています。

#define barrier() __asm__ volatile ("lwsync")
singleton *singleton::instance() {
    if (p == nullptr) {
        lock_guard<mutex> guard(lock_);
        barrier();
        if (p == nullptr) {
            p = new singleton();
        }
    }
    return p;
}

通常はCPUが提供する命令を呼び出します。この命令の役割は、この命令より前の命令をこの命令より後へ入れ替えることをCPUに禁止することで、この命令は通常barrierと呼ばれます。上のコードの**asm**はこれがアセンブリ命令であることを示します。volatileは省略可能で、これを使う場合はコンパイラに対してこのアセンブリ命令の最適化を許可しないと宣言することになります。lwsyncはPOWERPCが提供するbarrier命令です。

6.静的ローカル変数

Scott Meyerは『Effective C++』の中で簡潔なシングルトンの書き方を提案しました

singleton &singleton::instance() {
    static singleton p;
    return p;
}
  • シングルスレッドでは、正しい。
  • C++11以降のバージョン(C++14など)のマルチスレッドでは、正しい。
  • C++11より前のマルチスレッドでは、正しいとは限らない。

理由は、C++11より前の標準ではローカルstatic変数のメモリモデルが規定されていなかったためです。したがってスレッドセーフではありませんでした。しかしC++11ではスレッドセーフです。新しいC++標準は、あるスレッドが変数を初期化している間、他のスレッドはその初期化が完了するまでアクセスを待たなければならないと規定しているからです。

上で使われたメモリオーダー:

  • memory_order_relaxed:緩和されたメモリオーダー。原子オブジェクトへの操作が原子的であることだけを保証する
  • memory_order_acquire:獲得操作。ある原子オブジェクトを読み取るとき、現在のスレッドのそれ以降の読み書き操作をこの操作より前に並べ替えることは許されず、他のスレッドが同じ原子オブジェクトを解放する前に行ったすべてのメモリ書き込みが現在のスレッドから見える
  • memory_order_release:解放操作。ある原子オブジェクトに書き込むとき、現在のスレッドのそれ以前の読み書き操作をこの操作より後に並べ替えることは許されず、現在のスレッドのすべてのメモリ書き込みが、同じ原子オブジェクトを獲得する他のスレッドから見える

7.Atomic

C++11より前のバージョンでは、ロックによってスレッドセーフなSingletonを実現する以外に、各コンパイラ組み込みのatomic operationを利用することもできます。

javaとc#は順序変更の問題を発見した後、キーワードvolatileを追加しました。p変数を宣言するときにvolatile修飾を付ける必要があり、コンパイラはそれを見て、ここでは並べ替えできない(必ず先にメモリを割り当て、次にコンストラクタを実行し、すべて完了してから代入する)と判断します。

一方、c++標準はずっと修正されなかったため、VC++は2005版でこのキーワードを追加しましたが、クロスプラットフォームではありません(マイクロソフトのプラットフォームのみサポート)。

そしてc++ 11では、これらの欠陥を根本から解消するため、マルチスレッドに適したメモリモデルが導入されました。ついに、クロスプラットフォームな解決策を実現する仕組みが手に入ったのです。

mutex singleton::lock_;
atomic<singleton *> singleton::p;

/*
* std::atomic_thread_fence(std::memory_order_acquire); 
* std::atomic_thread_fence(std::memory_order_release);
* 这两句话可以保证他们之间的语句不会发生乱序执行。
*/
singleton *singleton::instance() {
    singleton *tmp = p.load(memory_order_relaxed);
    atomic_thread_fence(memory_order_acquire);
    if (tmp == nullptr) {
        lock_guard<mutex> guard(lock_);
        tmp = p.load(memory_order_relaxed);
        if (tmp == nullptr) {
            tmp = new singleton();
            atomic_thread_fence(memory_order_release);
            p.store(tmp, memory_order_relaxed);
        }
    }
    return p;
}

注目すべきことに、上記のコードは獲得と解放という2つの比較的重要な用語を使用しています:

  • 獲得はメモリに対する読み取り操作で、現在のスレッドのそれ以降の読み書き操作をこの操作より前に並べ替えることは許されません。
  • 解放はメモリに対する書き込み操作で、現在のスレッドのそれ以前の読み書き操作をこの操作より後に並べ替えることは許されません。

acquire と release は通常ペアで現れます。その目的は、同じ原子オブジェクトに対するreleaseがacquireより前に起こった場合、releaseより前に行われたメモリ変更が、acquireより後のメモリ読み取りによってすべて見えることを保証することです。

8.pthread_once

unixプラットフォームであれば、atomic operationを使う以外に、C++11を使わない状況でpthread_onceによってSingletonを実現することもできます。

プロトタイプは次のとおりです:

int pthread_once(pthread_once_t once_control, void (init_routine) (void));

実装:

class singleton {
private:
    singleton(); //私有构造函数,不允许使用者自己生成对象
    singleton(const singleton &other);

    //要写成静态方法的原因:类成员函数隐含传递this指针(第一个参数)
    static void init() {
        p = new singleton();
    }

    static pthread_once_t ponce_;
    static singleton *p; //静态成员变量 
public:
    singleton *instance() {
        // init函数只会执行一次
        pthread_once(&ponce_, &singleton::init);
        return p;
    }
};

9.まとめ

本文ではいくつかのシングルトンパターンを説明し、スレッドセーフなシングルトンパターン、およびC++11を使わないスレッドセーフなシングルトンの実装方法(memory barrier、静的ローカル変数、pthread_once方式、C++11のatomic実装など)を説明しました。

最後に注目してほしいのは、上述のシングルトンクラスのデストラクタについては、ダブルチェックロックパターン+自動回収の実装を参照してください。クラスの中に静的ローカル変数を宣言しなければなりません。静的ローカル変数はグローバル変数と同じだと考えてよく、プログラム終了時にその静的ローカル変数のデストラクタが自動的に呼び出されます。だからこそ、シングルトンのデストラクタを直接書くのではなく、クラスの中でネストされたクラスを宣言・定義するのです。

10.参考文献

https://thorns.cn/2019/03/09/DCLP.html

https://www.cnblogs.com/liyuan989/p/4264889.html

https://segmentfault.com/a/1190000015950693

再利用可能なスレッドセーフ・シングルトンテンプレートクラス

issue #303 の要望に応えて:boost::container::singleton_defaultの考え方を参考に、CRTPテンプレート基底クラスを使えば、任意のクラスを1行でスレッドセーフなシングルトンにできます(完全なコードは singleton_default.hpp にあり、並行テストも含まれています):

template <class T>
class SingletonDefault {
public:
    static T& instance() {
        static T inst;   // C++11 起局部静态变量初始化线程安全
        return inst;
    }
    SingletonDefault(const SingletonDefault&) = delete;
    SingletonDefault& operator=(const SingletonDefault&) = delete;
protected:
    SingletonDefault() = default;
};

// 用法:继承即可
class MyConfig : public SingletonDefault<MyConfig> {
    friend class SingletonDefault<MyConfig>;
private:
    MyConfig() = default;   // 构造私有,交由基类创建
public:
    int value = 42;
};

// MyConfig::instance().value

コメント