スレッドごとにローカルなシングルトン(的なもの)

// TODO: あとでちゃんと動作確認 (なお、CodeContract 関連以外は動作確認済みである)

C# でスレッドごとに固有のインスタンスを提供できるようにするという小ネタ。

エントリの変更点

09/02/01
  • スレッドセーフさについて言及を増やした
  • エントリ内部を小見出しで分けた

ただの Singleton

なお、スレッドごと、でなく AppDomain*1ごとの Singleton で良ければ、

sealed class HogeClass{
    /// <summary>Singleton なインスタンス。</summary>
    public static readonly Instance = new HogeClass();
    
    /// <summary>コンストラクタを好きに呼ぶことは出来ない。</summary>
    private HogeClass(){ ... }
}
// 常に同じインスタンスが返ってくる
HogeClass.Instance;

という風に簡潔に実現できる。このような public static readonly を使った本来の意味でのシングルトンはスレッドセーフであり、かつ高速であるので、普通のシングルトンならばこれで問題ない*2

ここで、スレッドに関係なく同じインスタンスが返ってくるところを、下のイメージのようにするのがこのエントリの趣旨である。

// アクセスするスレッドごとに別のインスタンスが返される
// 同じスレッドからなら同じインスタンスが反ってくる
HogeClass.Instance;

ThreadStatic とシングルトン的なもの

これを実現するには、ThreadStatic 属性を付けた上で簡単なシングルトン実装をすればよい。

using System.Threading;

sealed class HogeClass{
    [ThreadStatic]
    private static HogeClass instance_;
    /// <summary>スレッドごとに固有のインスタンスを返します</sumamry>
    public static HogeClass Instance{
        if(instance_ == null) instance_ = new HogeClass();
        return instance_;
    } 
    
    private HogeClass(){ ... }
}

ポイントとしては、

[ThreadStatic]
private static HogeClass instance_ = new HogeClass();

のようにしては いけない という点が挙げられる。
なぜなら、

ThreadStaticAttribute でマークしたフィールドの初期値を指定しないでください。このような初期化は、クラスのコンストラクタの実行時に一度だけ行われるもので、関係するスレッドは 1 つだけです。

ThreadStaticAttribute クラス

でも触れられているが、static フィールドの初期値の代入はタイプイニシャライザ*3が走るときにだけ行われるので、static フィールドの初期値の代入はクラスへの初回アクセスなど*4を行ったスレッドについてしか static フィールドが初期化されないためである。

ThreadStatic 属性を付ける場合、フィールドには初期値を書かず、フィールドの初期値は default(*) であることを前提としたコードを書く必要がある。

スレッドセーフ

ちなみに、上にある

if(instance_ == null) instance_ = new HogeClass();

というコードは、普通の Singleton 実装としてはスレッドセーフでないという問題があるが、今回のコードは「スレッドごとに」処理するものであるために、スレッドセーフさについては心配ない。

とはいえ、シングルトンをスレッドごとに生成しても、スレッドをまたいでシングルトンインスタンスを持ち運ばれてしまう恐れがあるため、上のコードだけでは完全なスレッドセーフにはならない*5。完全なスレッドセーフにしたい場合

  • そもそも、Instance を public に公開しない(static なメソッドだけを public にする)
  • あるいは、インスタンスが生成されたスレッドと、メンバが呼ばれたときのスレッドが一致するかどうか常に確かめる

といった工夫が必要である。

CodeContract とアサーション

.net framework 3 までだと、後者(スレッドの不一致を常に確認する)を簡潔な方法では実現できないように思えるが、.net framework 4.0 であれば、CodeContract を用いて、

using System.Threading;

sealed class HogeClass{
    [ThreadStatic]
    private static HogeClass instance_;
   
    /// <summary>このインスタンスを作ったときのスレッド</sumamry>
    private Thread instanceCreateIn;
    /// <summary>スレッドごとに固有のインスタンスを返します</sumamry>
    public static HogeClass Instance{
        if(instance_ == null) instance_ = new HogeClass();
        return instance_;
    } 
    
    private HogeClass(){
        this.instanceCreateIn = Thread.CurrentThread;
    }

    /// <summary>インスタンスを作ったスレッドと異なるスレッドからアクセスしてはいけない</sumamry>
    [ContractInvariantMethod]
    private void AssertThread(){
        CodeContract.Invariant(this.instanceCreateIn == Thread.CurrentThread);
    }
}

といった風にすることで、インスタンスを作ったときのスレッドと同じスレッドからしかメンバが呼ばれていないかどうかを、自動的にチェックすることができるだろう。CodeContract については BCL Team Blog – Base types, Collections, Diagnostics, IO, RegEx… などが参考になる。

ちなみに、たとえば中身が空のメソッドのように、HogeClass の状態に一切依存しない HogeClass メンバがあった場合、最適化の結果として [ContractInvariantMethod] が呼び出されなくなり、そういったメンバに対しては CodeContract.Invariant によるチェックが走らなくなるかもしれない*6。しかし、その場合はそもそも HogeClass の状態に無関係に処理が進んでいるのだから、スレッドをまたいだアクセスをチェックしなくても実質上問題ないはずである。

*1:乱暴に言うとプロセスみたいなもの

*2:この一文は 09/03/01 に追加した

*3:型の static なフィールドの初期化コードや static なコンストラクタのこと

*4:AppDomain の設定やクラスの設定によって、どのタイミングでタイプイニシャライザが実行されるかは異なる

*5:あるスレッドから HogeClass.Instance を読み取って、そのインスタンスを別のスレッドに渡すことが出来るので

*6:呼び出しがインライン化され、さらに必要に応じて呼び出し自体が消滅する、といった最適化をやってくれるのではないかと思われる。