C# での Singleton についてまとめ

type initializer を使った Singleton パターンの実装のスレッドセーフ性について訊かれたことがあったので、それについて調べたことをメモってみた・・・らいつの間にかまとめっぽくなってしまったのであった。

履歴

  • 09/10/18: 誤字修正(beforefirstinit -> beforefieldinit)
  • 11/01/20: コード例の誤り修正、細かい表現の修正

基本

C# で singleton を実現する場合、基本的には以下のようにするのが、よく知られたイディオムである。

sealed class Singleton{
	public static readonly Singleton Instance = new Singleton();
	private Singleton(){ /* ... */ }
}

ただし、このコードには、以下の注意点がある。

  1. Singleton の static フィールドへの初回アクセス以前の「任意の時点」で Instance が初期化される
  2. side-effect などについてスレッドセーフ性が保証されない
  3. シングルトンの初期化が循環依存する場合に注意する必要
  4. Instance 取得時の処理のカスタマイズ性の問題
  5. シリアライズ時にインスタンスが増えてしまう問題

それぞれの注意点について詳細を以下に述べ、最後にガイドライン的なものを示す。

フィールドへの初回アクセス以前の「任意の時点」で Instance が初期化される

これについてはネット上の既存の多くの記事でも言われているが、先述のコードでは Instance の初期化(Instance = new Singleton())は Singleton の static フィールドへの初回アクセス「かそれ以前の任意のタイミング」で行われる。

なぜなら・・・

この挙動が望ましくないのなら、以下のように static コンストラクタを定義する手がある。

sealed class Singleton{
	public static readonly Singleton Instance = new Singleton();
	static Singleton(){} // suppress beforefieldinit
	private Singleton(){ /* ... */ }
}

このようにすることで、type initializer に beforefieldinit 属性が付加されなくなる。それによって、Instance の初期化は、Singleton.Instance に初めてアクセスした瞬間に行われることが保証される。

なお、ネット上の記事などでは inner class を用いることで初期化タイミングを保証するといった記述もあるが、それらは CLI 仕様ではなく実装依存な挙動であり、実行環境に依存するので注意が必要である。
ただし、ここで述べている static コンストラクタを書くという方法も、C# コンパイラの(beforefieldinit 属性についての)実装依存であるので、コンパイラに依存しないコードを書く場合には注意が必要である。

side-effect などについてスレッドセーフ性が保証されない

beforefieldinit 属性が付加されることによるもう一つの注意点として、スレッドセーフ性についての保証が破られうるという問題がある。

beforefieldinit 属性がついていないのなら、

  1. 確実に一回だけ、type initializer が呼ばれる
  2. type initializer が完了するまで、他のスレッドが Singleton.Instace にアクセスしたとしても待たされる

ということが保証されるが、beforefieldinit 属性がついていると、CLI 仕様ではこれらが保証されない。これは、type initializer のパフォーマンスを稼ぐための beforefieldinit の(CLI)仕様によるものである(単純に side-effect なしで static フィールドを初期化するためだけの type initializer のパフォーマンスを稼ぐため)。詳しくは Type initializer についてまとめ (同一 blog) なども参照のこと。

そのため、マルチスレッドに Singleton.Instance にアクセスされうる場合には、beforefieldinit を付けさせないようにする(→ static コンストラクタを書いておく)必要があるだろう。

シングルトンの初期化が循環依存する場合に注意する必要

以下のように二種類(かそれ以上)の Singleton なクラスがあり、それらの初期化処理が相互依存している場合には、ある Singleton 初期化処理中にもう片方の Singleton の参照が得られない(null になる)ことがあり得るので注意が必要である。

sealed class SingletonA{
	public static readonly SingletonA Instance = new SingletonA();
	private SingletonA(){
		SingletonB.Instance ...
	}
}
sealed class SingletonB{
	public static readonly SingletonB Instance = new SingletonB();
	private SingletonB(){
		SingletonA.Instance ...	
	}
}

この場合、Singleton{A, B} の type initializer から間接的に呼ばれている Singleton{B, A} の type initializer が実行完了しているかどうかが保証されないため、上のコードで Singleton{A, B}.Instance にアクセスした結果が null になりうる。

Instance 取得時の処理のカスタマイズ性の問題

先述のコードの場合、Instance フィールドが露出しているため、後からプロパティに差し替えた場合、依存しているコードの再コンパイルが必要になりうる。
そのため、この Singleton のコードに依存するアセンブリがあり、かつそれらが Singleton のコードとは別にコンパイルやリリースされるのであれば、以下のようにプロパティでラップしておくべきだろう。

sealed class Singleton{
	private static readonly Singleton _instance = new Singleton();
	public static Singleton Instance{ get{ return _instance; } }
	private Singleton(){ /* ... */ }
}

シリアライズ時にインスタンスが増えてしまう問題

シリアライズ可能なオブジェクトから Singleton が参照されていたり、Singleton それ自体をシリアライズ可能にする場合、[Serializable] 属性を付けることになる。
しかし、単に [Serializable] 属性を付けただけの場合、Singleton がデシリアライズされる際に Singleton の新しいインスタンスが作られてしまうという問題がある。

これを防ぐためには、ISerializable インタフェースを実装した上で、SerializationInfo.SetType や IObjectReferece を用いることで対処することが出来る。
具体的なコードは、ISerializable Interface に詳しいのでそれを参照のこと。

ガイドライン

上記を踏まえると、C# で Singleton を実現する場合、エントリ先頭で示した基本となるコードに加えて、以下のようにした方が良いのだろう。

  • 以下の場合には、static コンストラクタを必ず書く (beforefieldinit 属性を付けさせない)
    • スレッドセーフな Singleton にする必要がある場合
    • Singleton の初期化タイミングを、確実に Instance アクセス時にしたい場合
    • ただし、コンパイラ非依存なコードにしたい場合には、static readonly ではなく lock などを用いた実装にする
  • Singleton の初期化処理が相互に依存しないように気をつける
    • それが難しいのなら、初期化処理にて他の Singleton の参照が null になることを考慮したコードを書く
    • beforefieldinit 属性を付けさせないことによってタイミングを保証させるのも一つの手である
  • 別のコンパイル/リリース単位から参照される場合、プロパティでラップしておく
  • Singleton を [Serializable] にする場合、単純にシリアライズさせないようにする