Patterns & Practices


C# でのシングルトンの実装

Microsoft Corporation

June 2003

日本語版最終更新日 2004 年 4 月 21 日

コンテキスト

C# でアプリケーションを作成します。インスタンスが 1 つだけあるクラスを作成し、このインスタンスへのグローバル アクセス ポイントを設定する必要があります。作成するソリューションは必ず効率的なものにし、Microsoft® .NET の共通言語ランタイム機能を利用するようにします。また、スレッド セーフなソリューションにします。

実装の方針

シングルトン (Singleton) は比較的シンプルなパターンですが、実装によって、さまざまなトレードオフやオプションがあります。以下に、一連の実装方針を示し、それぞれの長所と短所について説明します。

シングルトン

シングルトン デザイン パターンの次の実装は、『Design Patterns: Elements of Reusable Object-Oriented Software』 [Gamma95] で示しているソリューションに従ったものですが、次に示すように、C# で利用できる言語機能を利用するように、プロパティなどが変更されています。

using System;

public class Singleton
{
   private static Singleton instance;

   private Singleton() {}

   public static Singleton Instance
   {
      get 
      {
         if (instance == null)
         {
            instance = new Singleton();
         }
         return instance;
      }
   }
}

この実装には、主な長所が 2 つあります。

  • インスタンスが Instance プロパティ メソッドの内部に作成されるため、このクラスで追加の機能 (サブクラスのインスタンス化など) を実行することができます。ただし、この場合、厄介な依存関係の問題が発生する場合があります。

  • この実装では、オブジェクトからインスタンスを要求されたときに初めてインスタンス化が行われるため、このアプローチは "遅いインスタンス化" という特徴を持っています。遅いインスタンス化により、アプリケーションの起動時に不要なシングルトンの初期化が行われなくなります。

この実装の主な短所は、マルチスレッド環境の場合、安全性が確保されないことです。個々の実行スレッドが Instance プロパティ メソッドに同時にアクセスした場合、Singleton オブジェクトの複数のインスタンスが作成されることが考えられます。それぞれのスレッドが次のステートメントを実行して、新しいインスタンスの作成を要求する可能性があります。

if (instance == null) 

この問題はさまざまなアプローチで解決することができます。1 つは、ダブルチェック ロッキング (Double-Check Locking) [Lea99] と呼ばれるイディオムを使用するアプローチです。しかし、C# と共通言語ランタイムを組み合わせて使用した場合、静的な初期化によるアプローチを利用できます。このアプローチでは、スレッド セーフに対応するコードを開発者が明示的に使用しなくても、これらの問題を回避することができます。

静的な初期化

デザイン (Design) パターン [Gamma95] で静的な初期化を避けた理由の 1 つは、C++ の仕様では静的変数の初期化順序に、ある種のあいまい性が残っていたためです。幸い、.NET Framework では、次のような変数の初期化処理によって、このあいまい性を解決しています。

public sealed class Singleton
{
   private static readonly Singleton instance = new Singleton();
   
   private Singleton(){}

   public static Singleton Instance
   {
      get 
      {
         return instance; 
      }
   }
}

この実装方針では、インスタンスは、クラスのいずれかのメンバが初めて参照されたときに作成されます。変数の初期化は共通言語ランタイムによって行われます。クラスは、インスタンスの追加を招くことを防止するために "シールド" としてマークされます。クラスのシールド化の長所と短所については、[Sells03] を参照してください。さらに、変数は "読み取り専用" としてマークされるため、(ここで説明している) 静的な初期化時またはクラス コンストラクタでのみ、値の代入が可能になります。

この実装は前出の例に似ていますが、共通言語ランタイムを使用して、変数の初期化を行う点に関しては、前出の実装と異なります。ただし、この実装でも、シングルトン パターンで解決が試みられている基本的な 2 つの問題であるグローバル アクセスとインスタンス化制御に対処しています。まず、静的なパブリック プロパティにより、インスタンスへのグローバル アクセス ポイントが提供されます。また、プライベート コンストラクタを使用することで、Singleton クラスがクラスの外部でインスタンス化できなくなっています。したがって、変数はシステム内に存在するインスタンスのみを参照します。

Singleton インスタンスは静的なプライベート メンバ変数によって参照されるため、Instance プロパティに対する呼び出しによってクラスが最初に参照されるまで、インスタンス化は行われません。したがって、このソリューションでは、シングルトンのデザイン パターンのフォームの場合と同じように、"遅いインスタンス化" プロパティのフォームが実装されます。

このアプローチで予測される唯一の欠点は、インスタンス化のメカニズムに対する制御が難しくなることです。デザイン パターンのフォームでは、既定以外のコンストラクタを使用したり、インスタンス化の前に他のタスクを実行したりすることが可能でした。このソリューションでは .NET Framework によって初期化が行われるため、これらのことは実行できません。ほとんどの場合、静的な初期化は .NET でのシングルトンの実装で優先されるアプローチです。

マルチスレッド シングルトン

静的な初期化はほとんどの状況に適応します。アプリケーションでインスタンス化を遅らせる必要がある場合や、既定以外のコンストラクタを使用したり、インタンス化の前に他のタスクを実行したりする必要がある場合、またマルチスレッド環境で動作する必要がある場合は、別のソリューションが必要になります。しかし、静的な初期化の例のように、共通言語ランタイムによってスレッド セーフを確保することができない場合もあります。そのような場合は、特別な言語機能を使用することで、スレッドが複数存在するときに作成されるオブジェクトのインタンス数を確実に 1 つに制限する必要があります。一般的なソリューションの 1 つは、ダブルチェック ロッキング [Lea99] イディオムの使用によって、個々のスレッドがシングルトンの新しいインスタンスを同時に作成するのを阻止するようにすることです。

: 共通言語ランタイムでは、他の環境との共通ダブルチェック ロッキングに関連する問題を解決しています。この問題の詳細については、以下のメリーランド大学コンピュータ サイエンス学部の Web サイト「The 'Double-Checked Locking Is Broken' Declaration」を参照してください。(http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.htmlLeave-ms)

次の実装では、Singleton のインスタンスがまだ作成されていないときに、lock ブロックで識別される重要な領域に、単一のスレッドのみがアクセスできるようになっています。

using System;

public sealed class Singleton
{
   private static volatile Singleton instance;
   private static object syncRoot = new Object();

   private Singleton() {}

   public static Singleton Instance
   {
      get 
      {
         if (instance == null) 
         {
            lock (syncRoot) 
            {
               if (instance == null) 
                  instance = new Singleton();
            }
         }

         return instance;
      }
   }
}

このアプローチでは、必ず 1 つのインスタンスのみが作成されるようになっています。また、この作成はインスタンスが必要となったときに初めて行われます。さらに、変数が volatile として宣言されていることで、インスタンス変数への代入が完了するまでは、インスタンス変数にアクセスできなくなっています。最後に、このアプローチでは、syncRoot インスタンスをロックし、この型そのものをロックしないことで、デッドロックの発生を回避しています。

このダブルチェック ロッキング アプローチでは、Instance プロパティ メソッドの呼び出しごとの排他ロックを回避しながら、複数のスレッドの同時発生に関連する問題が解決されます。また、オブジェクトが初めてアクセスされるまで、インスタンス化を遅らせることも可能です。実際には、アプリケーションでこのタイプの実装が必要になることは稀です。ほとんどの場合は、初期化アプローチで充分に対応できます。

結果のコンテキスト

C# でのシングルトンの実装には、次の長所と短所があります。

長所

  • 静的変数の初期化をいつ、どのように行うかが .NET Framework によって明示的に定義されるため、静的初期化アプローチが可能です。

  • 「マルチスレッド シングルトン」の最初の部分で説明した "ダブルチェック ロッキング" イディオムが共通言語ランタイムに適切に実装されます。

短所

マルチスレッド アプリケーションで明示的な初期化が必要な場合は、スレッド処理の問題を回避する予防措置をとる必要があります。

参考

[Gamma95] 『Design Patterns: Elements of Reusable Object-Oriented Software』 Gamma、Helm、Johnson、Vlissides 共著 (Addison-Wesley、1995 年)

[Lea99] 『Concurrent Programming in Java, Second Edition』 Doug Lea 著 (Addison-Wesley、1999 年)

[Sells03] 「Sealed Sucks」 Chris Sells (sellsbrothers.com News : http://www.sellsbrothers.com/news/showTopic.aspx?ixTopic=411Leave-ms )

: 「Sealed Sucks」の記事は実際のところ、表題にかかわらず、クラスを "シールド" としてマークすることに関する賛否両論を均等に掲載したものです。

Patterns Practices

Page view tracker