.net framework での現在時刻取得のオーバーヘッドについてのメモ

.net にて、タイムスタンプ*1を低精度で良いので高速に得たかったのだが、どの方法が早いのかについてあまり情報がなかったので、やってみた。

やった実験

以下のプロパティをそれぞれ 1048576 (2^20) 回呼び出すメソッドを 4 回呼び出すコードを Debug ビルドし、 Windows 7 (32 bit) 環境にて実行した。

  • (空)
    • なにもしない空のループを回す
  • Environment.TickCount
  • DateTime.Now.Ticks
  • DateTime.UtcNow.Ticks
  • Stopwatch.ElapsedMilliseconds

結果(概略)

2〜3 回目の結果を集計すると、以下のようになった(全データは後述)。

プロパティ 2^20 回 [ms] 一回当たり [μs/call]
Environment.TickCount 2.53 0.0024
DateTime.Now.Ticks 156.85 0.15
DateTime.UtcNow.Ticks 18.84 0.018
Stopwatch.ElapsedMilliseconds 43.44 0.041
  • 2^20 回 [ms]: (ループ全体所要時間の平均) - (空ループ 2^20 回の平均)
  • 一回当たり [μs/call]: 2^20 回、の値を 2^20 で割った値 (マイクロ秒に換算)

所感

  • Environment.TickCount が高速である
    • DateTime.UtcNowの 7 倍以上、Stopwatch の 17 倍以上
  • DateTime.Now はかなり遅い
    • ローカル時間への変換が遅いのでは、とのこと*2
    • 32bit 環境であることも影響しているのかもしれない*3
  • 実は Stopwatch はそれほど極端には遅くない
    • 高精度な代わりに遅い、と巷ではよく言われているが・・・
    • DateTime.UtcNow の 2.3 倍程度である
    • DateTime.Now よりははるかに速い

要するに、とにかく高速に時間を取得したい場合には、Environment.TickCount を使った方がよさそうだ。
ただし、Environment.TickCount は約 24.9 日であふれる*4という仕様であり、いまどき 30 日程度連続稼働する PC など珍しくはない(はず)なので、扱いにとても注意が必要である。たとえ 30 日も連続稼働しないであろうプログラムでも、約 24.9 日の境界をまたぐと値の大小関係が狂うので注意が必要であることに変わりがない。

また、高精度タイマーが遅いという先入観を根拠とした判断*5は必ずしも妥当ではないことも頭の片隅に置いておいてよいだろう。

加えて、ついつい使ってしまいがちな DateTime.Now は、ローカル日時が必要な場合に限って使うべきであり、ある二つの時点の時間差を知る用途には UtcNow (か Stopwatch や Environment.TickCount)を使うべきであろう。

結果(詳細)

Empty roop: 3.5261 [ms], 3.36275100708008E-06 [ms/call] 
Environment.TickCount: 6.346 [ms], 6.05201721191406E-06 [ms/call] 
DateTime.Now.Ticks: 162.7851 [ms], 0.000155243968963623 [ms/call] 
DateTime.UtcNow.Ticks: 22.4571 [ms], 2.14167594909668E-05 [ms/call] 
Stopwatch.ElapsedMilliseconds: 54.7306 [ms], 5.21951675415039E-05 [ms/call] 
---------------- 
Empty roop: 5.4742 [ms], 5.22060394287109E-06 [ms/call] 
Environment.TickCount: 6.6567 [ms], 6.34832382202149E-06 [ms/call] 
DateTime.Now.Ticks: 163.9675 [ms], 0.000156371593475342 [ms/call] 
DateTime.UtcNow.Ticks: 21.4703 [ms], 2.04756736755371E-05 [ms/call] 
Stopwatch.ElapsedMilliseconds: 46.5926 [ms], 4.44341659545898E-05 [ms/call] 
---------------- 
Empty roop: 3.1857 [ms], 3.03812026977539E-06 [ms/call] 
Environment.TickCount: 6.5811 [ms], 6.27622604370117E-06 [ms/call] 
DateTime.Now.Ticks: 149.1435 [ms], 0.000142234325408936 [ms/call] 
DateTime.UtcNow.Ticks: 23.2336 [ms], 2.21572875976563E-05 [ms/call] 
Stopwatch.ElapsedMilliseconds: 45.3947 [ms], 4.32917594909668E-05 [ms/call] 
---------------- 
Empty roop: 3.1815 [ms], 3.03411483764648E-06 [ms/call] 
Environment.TickCount: 6.204 [ms], 5.91659545898438E-06 [ms/call] 
DateTime.Now.Ticks: 169.2678 [ms], 0.00016142635345459 [ms/call] 
DateTime.UtcNow.Ticks: 23.645 [ms], 2.25496292114258E-05 [ms/call] 
Stopwatch.ElapsedMilliseconds: 50.1856 [ms], 4.78607177734375E-05 [ms/call] 
---------------- 
|tickCount|2.53346666666667|0.00241610209147135| 
|dt_ticks|156.8458|0.149579811096191| 
|dt_UTCticks|18.8358333333333|0.0179632504781087| 
|sw_ms|43.4438333333333|0.0414312680562337|

実験に使ったコード

public void Test() { 
     this.result["tickCount"] = new List<TimeSpan>(); 
     this.result["dt_ticks"] = new List<TimeSpan>(); 
     this.result["dt_UTCticks"] = new List<TimeSpan>(); 
     this.result["sw_ms"] = new List<TimeSpan>(); 
     for (var i = 0; i < 4; i++) { 
          TestCore(); 
          Console.WriteLine("----------------"); 
     } 
     foreach (var name in this.result.Keys) { this.ShowSummary(name); } 
} 

private List<TimeSpan> empty = new List<TimeSpan>(); 
private Dictionary<string, List<TimeSpan>> result = new Dictionary<string, List<TimeSpan>>(); 
private void ShowSummary(string name) { 
     var sum = result[name].Skip(1).Average((ts) => ts.TotalMilliseconds) - empty.Skip(1).Average((ts) => ts.TotalMilliseconds); 
     var per = sum / Times; 
     Console.WriteLine(string.Format("|{0}|{1}|{2}|", name, sum, per * 1000)); 
} 

private const int Times = 1024 * 1024; 
private void TestCore() { 
     var sw = new Stopwatch(); 

     sw.Start(); 
     for (var i = 0; i < Times; i++) { 
     } 
     sw.Stop(); 
     Console.WriteLine("Empty roop: {0} [ms], {1} [ms/call]", sw.Elapsed.TotalMilliseconds, sw.Elapsed.TotalMilliseconds / (float)Times); 
     this.empty.Add(sw.Elapsed); 

     sw.Restart(); 
     for (var i = 0; i < Times; i++) { 
          var t = Environment.TickCount; 
     } 
     sw.Stop(); 
     Console.WriteLine("Environment.TickCount: {0} [ms], {1} [ms/call]", sw.Elapsed.TotalMilliseconds, sw.Elapsed.TotalMilliseconds / (float)Times); 
     this.result["tickCount"].Add(sw.Elapsed); 

     sw.Restart(); 
     for (var i = 0; i < Times; i++) { 
          var t = DateTime.Now.Ticks; 
     } 
     sw.Stop(); 
     Console.WriteLine("DateTime.Now.Ticks: {0} [ms], {1} [ms/call]", sw.Elapsed.TotalMilliseconds, sw.Elapsed.TotalMilliseconds / (float)Times); 
     this.result["dt_ticks"].Add(sw.Elapsed); 

     sw.Restart(); 
     for (var i = 0; i < Times; i++) { 
          var t = DateTime.UtcNow.Ticks; 
     } 
     sw.Stop(); 
     Console.WriteLine("DateTime.UtcNow.Ticks: {0} [ms], {1} [ms/call]", sw.Elapsed.TotalMilliseconds, sw.Elapsed.TotalMilliseconds / (float)Times); 
     this.result["dt_UTCticks"].Add(sw.Elapsed); 

     sw.Restart(); 
     var stopwatch = new System.Diagnostics.Stopwatch(); 
     for (var i = 0; i < Times; i++) { 
          var t = stopwatch.ElapsedMilliseconds; 
     } 
     sw.Stop(); 
     Console.WriteLine("Stopwatch.ElapsedMilliseconds: {0} [ms], {1} [ms/call]", sw.Elapsed.TotalMilliseconds, sw.Elapsed.TotalMilliseconds / (float)Times); 
     this.result["sw_ms"].Add(sw.Elapsed); 
}

*1:日時でなくても、時間の差が分かればよい

*2:http://stackoverflow.com/questions/62151/datetime-now-vs-datetime-utcnow

*3:TickCount は Int32 だが、DateTime.Ticks などは Int64 である。

*4:さらにほおっておくと、値が過去の値と重なる

*5:パフォーマンスと扱いやすさの両立のために Stopwatch を避けて DateTime を使う、など

VisualStudio 2010, 2008, 2005 のソリューション(.sln や .csproj など)を強引にコンバートするツールを作ってみた

作ったもの

  • SUtils.Development.VS.SolutionConverter.exe
    • .sln や .なんたらproj (.csproj や .vbproj など) をコンバートできる。
    • VS2010 の .sln などを VS2008 向けにコンバートしてビルド&動作までは確認
      • ただしいろいろと強引
      • 自分の使ってない VS の機能についても、それなりに大丈夫そうな気がするが、気がするだけである。
  • SUtils.Development.VS.FileFormat.dll
    • .sln のパーサーなど
    • VS2010 や VS2008、VS2005 のはき出す .sln を読み書き可能
      • アドオン(AnkhSVN とか)が付加する情報なども欠損せずに取り扱い可能

SolutionConverter.exe

VS2010 のソリューションから VS2008 のソリューションを機械的に作り出したかったので作った。
(自分の開発は VS2010 でやるが、VS2008 でもコードをいじれるようにしておく必要があったため。)

使いどころ


特に、

  • 同一のコードベースについて VS2010 と VS2008 版のソリューションとプロジェクトを用意したい
    • .cs や .vb といったコードは一つのファイルで、それを異なる VisualStudio 向けのプロジェクトから参照する
    • 上の画像のような感じに、同じフォルダに .sln や .なんたらproj が複数並ぶようにする
  • 新しい方の VisualStudio のソリューションから古い方の VisualStudio のソリューションを生成したい
    • アップグレードウィザードの逆
  • その他、VisualStudio の機能からはできない書き換えを、多少強引でも実行したい

という場合に使えるであろうツールである。

使い方

バイナリ: http://my-svn.assembla.com/svn/SToolsMirror/Development.VS.SolutionConverter
ソースコード: http://www.assembla.com/code/SUtils/subversion/nodes/trunk/CSharp/SUtils 以下

  • バイナリを落とす
  • SUtils.Development.VS.SolutionConverter.exe をたたく
    • SUtils.Development.VS.SolutionConverter.exe --help で簡易ヘルプが出る。

コマンドの例

SUtils.Development.VS.SolutionConverter.exe --sln=SUtils.sln --ver=vs2008 --fx=3.5

このように呼び出した場合、SUtils.sln ソリューションとそこから参照されているプロジェクトを全て読み込み、それらを

に変更して、SUtils.vs2008.sln などとして書き出す。

SUtils.Development.VS.SolutionConverter.exe は、VisualStudio (2005, 2008 or 2010) ソリューションファイルを読み込んで、VisualStudio (2005, 2008 or 2010) ソリューションファイルをはき出すことができる。
また、その際に、ソリューションから参照されているプロジェクトもコンバートし、プロジェクトの参照設定なども修正する。

注意点

当たり前だが、.net 4.0 にしかないメソッドや、.net 4.0 にしかないアセンブリ .net 3.x で使えるようになる!といったことは無理である。

そのため、コンバート先のフレームワークに存在しないメソッドなどは当然コンパイルエラーになる。
SolutionConverter.exe は、ビルド設定を書き換えて定数を追加 #define する機能がある(--const 引数で指定)ので、これを与えた上で、#ifdef を使う必要があるだろう。

// NET_3_x と NET_3_5 定数を追加で定義
// C# 側のコードでは、バージョン依存のコードを適宜 #ifndef NET_3_x なりでくくる。
SUtils.Development.VS.SolutionConverter.exe --sln=SUtils.sln --ver=vs2008 --fx=3.5 --const=NET_3_x,NET_3_5

また、VisualStudio がサポートしてない行為であるし、そもそも実現手段からして強引であるので、当然結果を保証できない。
大事なファイルに対して適用する場合は、バックアップなりを事前にとっておいた方が・・・。

マニアックな点

SolutionConverter.exe では、ほかにも以下の機能に対応している。

  • アセンブリ参照の強制削除(デフォルト: --refDel="System.Xaml, Microsoft.CSharp")
  • ファイル名の変換規則の変更(--slnFileName および --projFileName オプション)
    • SolutionConverter.exe が書き出す .sln や .なんたらproj のファイル名のパターンを変更する。
    • デフォルトは "{0}.{2}.{1}" (ファイル名.VSプロダクト名.拡張子)
    • String.Format のフォーマット文字列になっている。
    • ここを "{0}.{1}" (元のファイル名.拡張子) にすると、ファイルを上書きするようになる。
  • .sln に関連づけられたエディションの変更(--edi オプション)
    • 例えば、"Visual Studio 2010 に関連づけ" "Visual Basic Express 2005 に関連づけ" のように、エディションを変更できる。
    • "--edi=C#" や "--edi=Studio" のようにして指定。

SUtils.Development.VS.FileFormat.dll

ダウンロード元などは SolutionConverter.exe と同じ。
これの入っている SUtils は Apache ライセンスなので、ライセンスの範囲内でご自由にどうぞ。

Mac OS X Leopard (10.5) から Snow Leopard (10.6) へ移行したことで発生した問題めも

最近、メインのノートPC(macbook pro)の環境を Snow Leopard へ移行*1したのだが、さまざまな問題が発生して苦しんだ。
ので、後々のためにも、Snow Leopard になって劣化した部分についてメモってみた。

スリープから復帰しなくなった

症状
  • bluetooth キーボードを接続したままの状態でスリープさせる。
  • すると、スリープから復帰しない。
    • 画面が真っ暗で、マウスカーソルも出ない。
解決策と副作用
  • "Allow Bluetooth devices to wake this computer" を off にする。
    • System Preference -> Bluetooth -> Advanced
    • Bluetooth キーボードやマウスからスリープ解除できなくなるので不便・・・
前からあった問題: スリープ中にデバイスを脱着すると危ない

筆者は、Bluetooth キーボードと外付けディスプレイ(DisplayPort -> DVI-D)を macbook に繋いで自宅にて使っている。
そして、外出する際には、それら外付けデバイスを外してから macbook をスリープさせて持ち出すようにしていた。
わざわざ外付けデバイスを外してからスリープしているのは、スリープ中にデバイスが増えたり減ったりすると、レジュームに失敗することがあるためである(画面が真っ暗かつ無反応になる)。
この問題は、Leopard (10.5) の頃から存在し、Snow でも治っていなかった。
解決策としては、スリープ前に外付けデバイスなどを外すようにするぐらいであろう。
今回の bluetoothバイスの問題は、Snow Leopard でこの問題がより酷くなったものと推測される。

スリープからの復帰後にマウスカーソルしか出なくなった

発生する条件と症状
  • 外付けディスプレイをメイン画面に設定してあって、そのディスプレイを外してからスリープする。
    • メイン画面: メニューバーを表示する画面の意。
  • スリープやスクリーンセーバからの復帰時に、マウスカーソルだけが出た状態になる。
    • 画面は真っ暗。
解決策と原因(憶測)
  • "Requrie password (時間) after sleep or screen saver bigins" を off にする。
    • System Preference -> Security
    • スリープからの復帰時にパスワードを要求しないので、セキュリティ的に怖い・・・。

もしこの現象に遭遇してしまった場合、画面にはなにも出ていないがパスワード入力欄にフォーカスが当たっているので、落ち着いてパスワードを打ち込んで Enter を押すと、ロックを解除できる。

メイン画面に設定されてた外付けディスプレイが外されたことで、ノート PC 本体のディスプレイがメイン画面になったにも関わらず、ロック解除のためのパスワード入力画面が外付けディスプレイのあった座標に表示されてしまうのが原因かと推測される。

キーボードショートカットで仮想画面を切り替えると、キーボード操作が一切出来なくなるようになった

発生する条件と症状
  • Ctrl + (矢印キー) などのキーボードショートカットによって、Spaces の仮想画面を切り替える。
  • 仮想画面を切り替えたことを示す表示が消えなくなる。
  • あらゆるキーボード入力が効かなくなる。
    • ミュートやボリューム調整などの特殊なキーの一部だけが機能する。
解決策
  • Spaces をキーボードショートカットで使わない。
  • もし現象が発生してしまったら、あきらめてリブートする。
    • (10/04/21 追記) Finder を再起動するだけでも治る。
    • Finder を再起動するには、メニューバーのリンゴをクリックして、強制終了のためのメニューを出す。

メニューバーの一部が描画されなくなった

症状


  • メニューバーの一部が描画されなくなる。
    • 日本語入力の状態やバッテリー残量 .etc が見えなくなる
  • 原因や発生条件は不明
解決策
  • 絶賛募集中

EMOBILE HW Utility が動作しなくなった

症状
  • Snow Leopard 対応の EMOBILE HW Utility を入れても、"failed" とだけ言われて接続に失敗する
  • Snow Leopard 非対応バージョンだと、Utility が何も言わずに落ちてしまう。
解決策
  • 絶賛募集中
  • しょうがないので VMWare 上の windowse-mobile につないでます・・・

バックライトが点かない&キーやマウス入力を受け付けない現象が起こるようになった

// (10/04/21 追記)

症状
  • スリープから復帰させる
  • バックライトが点かない (よく見ると画面は表示されている)
  • キーやマウス入力に全く反応しない
  • macbook 限定の現象、だろう。

しばらく放置したが戻ってこなかった。

解決策
  • macbook の蓋を開閉してみる
    • してみたが、筆者の事例では効果がなかった。時間をおくといいのかもしれない・・・?

2010/05/01 追記

問題の Snow Loepard 環境について、以下の処置を施した。

  • Snow Leopard の修復(?)インストール
  • Spotlight を停止
    • メモリを 700 MB 以上食っていたので

その上で、ここまでに述べた、確実に固まってしまう運用はしないように心がけた。
その結果、現在までにはフリーズなどを経験することなく、まともに運用できている。

*1:といっても、クリーンインストールである。

Type initializer についてまとめ

参考資料

  • ECMA-335 (4th edition)

Type initializer とは

Type initializer は CLI 仕様で規定されている、型それ自体を初期化するための特別なメソッドである*1

要するに(C# で言うところの) static メンバの初期化子や static コンストラクタを実行してくれるアレである。

CLI 仕様書(ECMA-335)の 10.5.3 (Type initializer) 以下では、下に述べる事柄が述べられている。

Type initializer の満たすべき条件

Type initializer には以下の制約が与えられている。これらは、普通の C# コンパイラを使ってコード生成する限りは気にする必要はないだろう。

  • static である
  • パラメタを持たない
  • 戻り値を持たない
  • rtspecialname, specialname を持ち、".cctor" という名前である

Type initializer で出来ること

Type initializer では、普通のメソッドとして出来ること以外に以下の事ができる。

  • initonly 属性を持つ static フィールドに書き込める

Type initializer の実行について、CLI によって保証されること

Type initializer の実行については、以下の点が守られることが、CLI 仕様によって保証されている(し、CLR*2 や mono はそれを守っている)。

共通して言えること

  • type initializer の実行タイミングについて
    • "all-zero" な値型や、null な参照型のメンバアクセスについてのみは、無視されうる
      • つまり、null なインスタンスのメンバにアクセスした場合などは、事前に type initializer が実行されるとは限らない
  • static field はいかなるアクセスよりも前に、known state に初期化されている
    • "all-zero" 値や null 値などであるということ

beforefieldinit 属性がないなら

  • 以下のうちいずれかの時に type initializer が実行される
    • その型の static field への初回アクセス時 または
    • その型の static method の初回の呼び出し時 または
    • その型のコンストラクタの初回の呼び出し時
  • 確実に一回だけ*3、与えられた型について実行される
    • ただし、ユーザーによって明示的に呼び出さた場合はこれは保証されない。
  • Type initializer の実行が完了していない型のフィールドには、type initializer によって呼ばれたメソッド以外はアクセスできない

beforefieldinit 属性でマークされているなら

  • 以下の時に type initializer が実行される
    • その型の static field への初回アクセス時 か それ以前

なお、beforefieldinit 属性がある場合、beforefieldinit ない場合で述べた事柄は保証されなくて良い。特に、最後の保証(type initializer の完了を待機する)が保証されないことがありうる。(ECMA-335 の 10.5.3.2 の Rationale 曰く、最後の保証は、複数 AppDomain 環境ではコストが高くつくうえ、滅多に必要がない*4ため、とのこと。)

また、この beforefieldinit の挙動は、初期化コードが特に有意な side-effect を持たない時のために用意されているそうだ(ECMA-335 8.9.5 の Note より)。

C# コンパイラの生成するコード

なお、C# コンパイラは、以下の処理を順番にひっつけて、Type initializer を生成する。

  1. static メンバの初期化処理
  2. static コンストラクタの中身

また、static コンストラクタが無い場合には、type initializer に beforefieldinit 属性を付加する。

*1:CLI 仕様(ECMA-335) 10.5.3 より、"a special method called a type initializer, which is used to initialize the type itself."

*2:.net framework で使われている CLI 実装

*3:executed exactly once

*4:多くのコードでは、type initializer は単にフィールドを初期化するためだけのものであることが理由だそうだ。

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] にする場合、単純にシリアライズさせないようにする

Net_Growl を使ってみた

ref: Net_Growl 2.7.0
ref: ネイティブMacアプリをPHPで操作しよう (3/3)- @IT
関連: OS X に macports で入れた pear (@ PHP 5.2.10) が使えない現象とそれへの対処 - やこ〜ん SuperNova2

せっかく pear のテストのために Net_Growl pear パッケージを入れてみたので、試しに使ってみた。

Net_Growl を使うと、php から簡単に Growl をたたくことができる。php で書かれたバッチ処理のためのスクリプトやらも持っている身としては、Growl で通知できればちょっと嬉しいものである。

必要な準備

% sudo pear install net_growl-beta

Growl 側の設定

@IT の記事 では触れられていないが、Net_GrowlUDP によって Growl への通知を行う関係上、Growl 側で UDP による通知を受け付けるように設定する必要がある。

具体的には、システム設定などから Growl の設定画面を開いた上で、Network タブで、

  • Listen for incoming notifications
  • Allow remote application registration

の二つを有効にしておく必要がある。

また、このままだと同一サブネット上のどこからでも、このコンピュータへと Growl の通知を投げつけることができてしまう。そのことが不気味であるなら、パスワードを設定しておくべきだろう。

書いてみたコード

<?php
require_once 'Net/Growl.php';

$growlApp = new Net_Growl_Application("AppName", array("NotifyA"), "Growl で設定したパスワード");
$growl = new Net_Growl($growlApp);
$growl->notify("NotifyA", "title", "body");
?>

これを Growl の動いているマシンで実行すると、Growl の通知がポップアップするであろう。

コードについてメモ

  • new Net_Growl() の第一引数は参照を受け取るので、Net_Growl_Application インスタンスの格納されている変数などを指定せねばならない
  • notify() の第一引数に指定する通知の名称は、Net_Growl_Application の第二引数の array にある文字列のどれかでなくてはならない
  • Growl 側でパスワードを設定しなかった場合、Net_Growl_Application の第三引数は不要である