System.Runtime.Remoting.Contexts.Context

先日 .NETでのAOP実装について紹介したとき(id:NyaRuRu:20040625#p2)(id:NyaRuRu:20040624#p1)も少し触れましたが,本来ContextBoundObjectは"Context"という概念を構築するために導入されたクラスです."Context"の詳しい説明については"Essential .NET"に譲りますが,.NET Frameworkのいくつかのサービスを実装するために利用しているのは確かなようです.一方,通常のアプリケーションはこれらの仕組みを直接利用すべきでないとされていて,公式ヘルプにもクラスの詳細は載っていません.
http://msdn.microsoft.com/library/default.asp?url=/library/en-us/cpref/html/frlrfSystemRuntimeRemotingContextsContextClassTopic.asp
http://lab.msdn.microsoft.com/library/default.asp?url=/library/en-us/cpref/html/T_System_Runtime_Remoting_Contexts_Context.asp
何故これが推奨されていないかも面白い話題ではありますが,ここでは触れないことにします.今回の本題は,公式なヘルプもサンプルも存在しないContextという仕組みを安全に利用するための予備調査です.一見便利に思える仕組みに意外な落とし穴が有ることは珍しくありません.例えば,C#コードを実行時にコンパイルできると知ったときにまず私が考えたのはゲーム用のスクリプトに利用することでした.しかしこれは後で述べる問題を抱えています.
http://www.atmarkit.co.jp/fdotnet/dotnettips/101compileinvoke/compileinvoke.html
実行時の動的なコンパイルはASP.NETの基幹技術でもあり,広く公開されているものです.しかし,実行時にアセンブリをロードすることがメモリリソースが消費につながり,アセンブリのアンロードがアプリケーションドメイン単位でしか行えないことの意味まで指摘している資料は多くはありません.結論としては,そのままではゲーム用のスクリプトには向いていません.meiさんが実験されているような工夫(id:akiramei:20040416#p1)が恐らく必要となるでしょう.
話を戻すと,Undocumentedな機能であればなおさらこのような落とし穴に一層の注意が必要です.私は近々本格的にContextを利用してみようと考えているプロジェクトがあって,それに向けてここしばらく予備調査を行っていました.ただし以下に示す結果と考察は私の手元の .NET Framework 1.1での挙動であって,公式な仕様に裏付けられたものではないことにご注意下さい.
まずContextの寿命について調べてみました.CLR Profilerでオブジェクトの寿命を観察したところ,Contextは参照が無くなればGCによって解放されることが確認できました.ContextBoundObjectが短命になるよう心がける限り,通常用途でContextオブジェクトがメモリ上に居座り続けること無いと考えて良いでしょう.メモリリソースに限っていえば,サーバロジックで接続単位にContextを生成したり,ゲームで敵キャラごとにコンテキストを生成することに回避不可能な致命的弱点は見つかりませんでした.
次に,各ContextBoundObjectをどのContextに所属させるべきかについて考えてみました.これは実装の複雑さとパフォーマンスとのトレードオフで決まります.ContextはContext Propertyという形でIContextPropertyを実装したオブジェクトを複数個登録することができます.プロパティはコンテキスト単位に存在し,名前で識別され,同じコンテキスト内で共有されます.Context PropertyはそのContextに属するオブジェクトに共通で成り立つ性質を記録するために使えます.Propertyの例として,スレッド優先度・セキュリティ権限・前提とするハードウェア状態などが考えられます.AOPの回で書いた割り込みもContext Propertyの一種として理解できます.複数個のContext Propertyを持つ場合,それらの状態の組み合わせを真の「状態」と考えることが出来ます.つまり,複数個のPropertyのうち1つでも異なれば「状態」が異なるとする立場です.「状態」ごとにContextを生成することで初めて,あるContextに結合したオブジェクトが同じ「状態」を持つことが保証されます.必ずこの立場を採用する必要は無いのかもしれませんが,私は実装からContextをそのようなものと解釈しました.以下ではこの考え方を採用することとします.継承先で新たな状態が追加されるというシナリオについては後述しますが,この方法はそのような未知の状態についても安定に振る舞います.
同じ「状態」に対応するContextが複数あってもかまいません.ただし異なるContextに所属するオブジェクト間のメソッド呼び出しは,Context切り替えに伴うオーバーヘッドが発生します.そのため「状態」が同一なContextは出来るだけ1つにまとめた方がパフォーマンス上は有利となります.
剰余類を用いた例を挙げてみましょう.1から100までの自然数に対応したクラスを考えてみます.まずは3で割った余りでこれらのクラスの分類してみましょう.代数学的には3つの状態が考えられます.この場合Contextは必ず3つ以上必要になります.一方必要なContextは高々100個です.この場合各オブジェクトにlocal fieldとして状態を持たせることと意味は変わりません.私が面白いと思うのは,Contextを用いると安直には100個必要な状態変数を3個以上のいくつかまで減らすことが出来ることです.さらに5で割った余りによる分類も付け加えてみましょう.この場合は3で割った余りと5で割った余りの組み合わせは,15通りの代数学的な「状態」を生成します.この場合Contextは最低15個必要です.問題が複雑化したとき,我々はいつでも100個のContextを生成する方法に逃げることが可能です.
具体的にこの過程を見てみましょう.ある束縛オブジェクトがどのContextに属するかはインスタンス生成時に決定され,以後異なるContextに再結合することはありません.生成時に選べるContextは2つあります.1つはnewobjオプコードが呼び出されたときのContextです.もう1つはその場で新規作成するContextです.以下のPowerPointファイルの30ページを見てください.
http://docs.msdnaa.net/ark_new/Webfiles/Courses/DNRK/Context_Remoting.ppt
ContextAttributeを継承したカスタム属性は,既存のContextを使うかどうかについて拒否権を行使することが出来ます.設定された属性*1のうち1つでもIsNewContextOKで偽を返した場合,新しいContextの作成が確定します.そして新しいContextが作られる場合,Contextに適切なPropertyを設定することもカスタム属性のみに与えられた権利であり義務です.Context PropertyはそのContextの「状態」を記述する本質的なパラメータであるため,Context作成時にのみ追加が許され以後プロパティの数は増やせません*2
各属性がIsNewContextOKを適切に実装していれば,最適ではないにしろContextは適切に分離されます.これは継承によって新たに属性を設定する際にありがたい性質です.このように未知の属性が付与される可能性がある場合,設計が実行時のContext分けに依存しないかどうかを確認することが重要です.
さて,最適化のためにオブジェクトを既に作成済みの特定Contextに所属させたいということもあるかもしれません.この場合当然ながらそのコンテキスト内部でインスタンス生成を行う必要があります.容易に思いつくのは対象Context内にファクトリクラスを作っておくという方法でしょう.トリッキーな手法としてContext.DoCallBackを使って目的のContext内部に生成コードを送り込む*3という方法もあります.

*1:ただし同じ型の属性を複数回設定することには対応していないようです.AttributeUsageでAllowMultiple = trueとすればコンパイルは通りますが実行時には1つしかコールバックされません.

*2:Contextには名前付きデータスロットや動的Propertyなどの動的にパラメータを追加・削除するための仕組みもあります

*3:Context.DoCallBackを使用すれば一見ContextBoundObjectの実行コンテキストを置き換えられそうです.しかし実験してみると分かりますがContextBoundObjectのインスタンスメソッドを指すデリゲートを渡した場合でも実行コンテキストはあくまでContextBoundObjectが結合しているものが使用されます.つまり特定Context内でnewobjオプコードを実行したい場合は,ContextAgileなデリゲートを使用する必要があります.