の事を書いていたら、猫にキーボード踏まれて消去 orz
気が向いたら書き直すか…
コードコントラクトと検証
.NET Framework 4 より、本格的にコードコントラクトがサポートされるようになったそうだ。
とはいっても、実際に検証するのは別のツールであって、コードにコントラクトが書けるように、標準のインターフェイスが準備されたことだ。
System.Diagnostics.Contracts名前空間以下のクラスを使用する。
public int Divide(int a, int b) { // 事前条件 Contract.Requires(b != 0); <pre><code>return a / b; </code></pre> }
要するに、Debug.Assert() していたような所をこれに置き換えていく。事後条件の場合は Contract.Ensures() を使う。戻り値をチェックしたい場合は、Contract.Result() で戻り値が参照出来る。
例えば、負の数が許されない加算であれば、
public int Add(int a, int b) { // 事前条件 Contract.Requires(a >= 0); Contract.Requires(b >= 0); <pre><code>// 事後条件 Contract.Ensures(Contract.Result() &gt;= 0); return a - b; </code></pre> }
という具合だ。
この時点で、「非常に魔術的」な香りが漂っている。特に、事後条件の Contract.Result() で戻り値が参照出来るだとか、そもそも事後条件が除算処理の手前にあったりして、本当にこれでいいのか?という感じがする。
実は、最初にインターフェイスが準備されただけ、と書いたのはこの部分の事で、このコントラクト定義を実際に使うには以下の条件を満たす必要がある。
・コンパイル時シンボル「CONTRACTS_FULL」が定義されていないと、Contractクラスのメソッド呼び出しは全て削除され、バイナリには一切含まれない。
・コントラクトの検証は、能動的には行われない。Debug.Assert() のように、書いた場所で直にコードが確認するわけではない。
実際には一部の評価は行われるが、結局のところは「バイナリリライター」と呼ばれるライブラリが介在する必要がある。
で、それでは、Debug.Assert() と比べて何が嬉しいのか?という話になるのだが(ようやく本題)、コードに書かれたコントラクトをメタデータ(とMSIL?)から抽出して、その情報を元にごにょごにょ出来るようになるというところが違う。
Microsoftの実装では、「Pex」というプロジェクトでコントラクトを利用している。Pexは、ユニットテストコードを自動的に生成するツールとライブラリで、コードからコントラクト定義を抽出して、事前・事後条件などからユニットテストが実行すべきテストパターンを計算、「テストコード」を吐く。
恐ろしいww
いや、もちろん、完全な自動化テストは不可能ということは分かっているのだが、特にこの場合はどれだけコントラクト定義が綿密に書けるかということと、いかに普段からテスト可能コードを書けるかと言う事に尽きるのだが、それにしてもとうとうやっちまったかという感じがする。
PexはVS2010のProfessional以上で使用可能で、MSDNサブスクライバダウンロードから入手可能。また、Ultimateでは標準で入っているので、Microsoftとしてももはやこのプロジェクトは実験段階を超えていると判断しているようだ。
より詳しく知りたい場合は、以下のサイトを参照されたし。
MSDNマガジン:クラスにソフトウェア コントラクトを導入する
MSDNマガジン:Visual Studio 2010 における Code Contracts の設定
Microsoft Reserch: Pex and Moles (なにげにMolesも凄いのだが)
VS2010 後の世代のプログラミング (2) Pex
他にも、コントラクト定義を使ったこんなのもある。
Contractor.NET | A .NET validation and specification strengthening tool
.NET Framework 4 未満の互換ライブラリを書いているのであれば、ここにある前身バージョンを使えば、コンパイルは通るようになります。
(.NET Framework 2 からOK。ただし、バイナリリライターはないので、検証は出来ません)
Windows 8 WDK Preview (Eliminated BUILD.EXE !!)
遂に、WDKはVisual Studioに統合されるようだ :-)
というのも、Preview WDKをインストールしてみたのだが、「build.exe」が無い!!!
いや、ちょっと困る orz とりあえず既存のドライバをビルドしてみたかったのだが、そのままでは出来ない。
しかし、その代わりに msbuild の拡張アセンブリと思わしきファイルや、VSの拡張ファイルなどがある。
OSRでもその手の話題が出始めている(真面目に見てないのでわからないが)。
問題は、ドキュメントが無い事だ。どう料理していいのかわからない。
Previewだからこんなもんか。
Visual Studioに統合されると言っても、VS2011 向けだ。
気になるのは、2000はアウトとして、XPはどうだろう?というところかな。
Windows 2000はすでにWDK6001で終わってしまっている。
今は仕方がないので、2000向けだけ6001で、あとは7000でビルドしている。
この方法は、良くも悪くもbuild.exeがあるから使えるわけだが、build.exeが綺麗さっぱりなくなるのであれば、いよいよ2000のフェードアウトを考えなければならないか…
うーん、1ビルド分は、build.exeシミュレータみたいなものを用意して欲しかった。
ConnectでWDKの悪口突っ込んでたから、今更そんなこと言えないwww
あと、残念な点は、ARM関係のファイルは入ってなかったってことだ :-)
これまた、とりあえずクロスコンパイラでビルドとかやってみたかったのだが…
ユーザーインターフェイスとシステム雑記だらだら
Windows 8(仮称)が発表され、MSDNサブスクライバダウンロードからプレビュー版をダウンロードできるようだ。
#今ダウンロード中。
#Akamaiのダウンローダも少しはまともになって、キューに入れたダウンロードタスクの殆どは
#同時並行でダウンロードするようになったようだ
Windows 8でMetroインターフェイスが導入されるのだが、心配している点がある。
Windows 2.1 (!) で「プログラムマネージャ」と「ファイルマネージャ」が導入された。
この時に「なんてダサいインターフェイスだ」と思った。
DOS世代のアプリケーションランチャーとファイラーをそのまま実装しただけだった。
Finderの足元にも及ばなかった。正直、「これが未来か!」という気は全くしなかったものだ。
なんという名前だったか忘れたが、Finderもどきのユーティリティがあったりしたが
結局純正ではなかったので、広まることもなかった(詰めも甘かった)。
DOS5で導入された「DOSシェル」も、結局は薄皮かぶったドブネズミ状態だったわけで、
もっと根本からリソースを共有するための、優れた手段を提供しなけれなならないのにと思っていた。
UNIXはその点では優れていたが、現代のUNIXクローンもまたそこから脱することは出来なかった。
#未だにshとcshの延長でしかないし。
#その世界で完結する作業をしている分にはすばらしいが、
#多様化するユーザーインターフェイスを一元的に扱う機能は提供されていない。
#シェルの強みはパイプラインとジョブコントロールだと思っているので、
#それを超える操作は、独自ライブラリ・独自APIの世界にならざるを得ず、
#突然相互運用性と応用性を失う。
私がどうもAndroidにもiOSにも萌えないのは、あのメニューがプログラムマネージャに「クリソツ」だからかもしれない。
(見た目はきれいだけど)
Windows 95で「エクスプローラ」が導入された。
ファイルセントリックで、コンテキストメニューで操作を提示できる点は進んだと思う。
しかし、「デスクトップ」と「マイドキュメント」の概念がダメダメだ。
ツリーでディスクとフォルダ構成を見せている以上、内部の構造がそのまま素直に見えるようにすべきだったと思う。
あれがひっくり返っている(デスクトップがすべての起点にある)のが諸悪の原因だ。
技術者ではないユーザーは、あれのためにコンピューターの中のイメージが持てなくなってしまった。
「ここのデスクトップは、実はこのフォルダの中のここが見えているんですよ」
「???」
何回こういう説明をしただろうか…
未だに、マイドキュメントの位置をDドライブに移動するという説明を理解してもらえないことが多い。
(メーカー製のPCはせっかくパーティションがわけてあっても殆ど無意味なのだ)
ユーザーの学習曲線について考える責任者がいなかったのだろうか、或いは分かっていてやっているのか。
デジタルディバイドという言葉があまりはやらなくなったが、「デジタル音痴」はいまだ多数を占める。
こういう層の人ほど、コンピューターの応用方法を早く習得できる必要があるのだ。
そのためには、ただでさえ分かりにくい「コンピューター」を、更に理解不能にする要素は排除する必要がある。
UNIXのシェルは多少ロジカルな思考が必要だが、一度理解すれば応用力はどんどん伸びていく。
Windows環境ではこういう経験はほとんどない。環境どころかAPIも一貫性が無いおかげで苦労する。
そしてGUIの世界が一般的になった時、UNIXクローンも同じような問題を抱えるようになった。
(向こうはもっと激しいよね。コミュニティがどんどんAPIを変更していくから、ついていく気が無いと難しいし、
基本的にデベロッパ向けだから一般人は知る事すらかなわない)
Windows Phone 7.5のMetroインターフェイスには少し期待している。
「一から作った」ということは、過去のWindowsの文化を全て捨てて考えることが出来たと言う事だ。
iOSのおかげ(?)で、本来ならMS Labs内で終わるようなプロジェクトにゴーサインが出たわけだ。
Peopleハブは正に、「Peopleセントリック」ということだろう。
新しいデバイスであるからこそ、既存のデバイスを打ち破る「文化」が無いと魅力を感じられない。
進化が無いなら、今までの環境で十分。新しいデバイスに投資するなら、それだけの価値がないとね。
(価値は個人によるので、他人はどう感じるかは別の問題。見た目カッコいい!で選ぶのもアリだろう)
Windows Phoneという小さいデバイスであまり夢を見ることもできないが、いっそ「メタセントリック」を扱えるように出来れば、
OSの応用性に未来が見い出せるのではないかと思う。
Windowsのメインプロダクトが目指してほしいところはここだ。
だが、Windowsのメインプロダクトは過去を捨てることが出来ない宿命にある。
Metroインターフェイスを導入すると言っても、それはつまりは「新しいプログラムマネージャ」ではないかという気がする。
ラップインターフェイスの成功例として、.NET Frameworkは良くできていると思う。
周りからの色々な圧力(?)に屈せず、よくあそこまでの完成度にたどり着いた。
しかし、当初検討されていた、Windows自体を.NET Frameworkで再構築するという大胆な案は、もう実現しそうにない。
それが本当に良い事かどうかも分からないが、マイクロソフトがその案をフェードアウトしたということは、
夢は見れたかもしれないが、やはりそれだけでは「商売」にならないということだろう。
それはつまり、まだしばらくはWin32とWDMからは逃れられないと言う事だ。
そうなると、新しい機能はすべからく「ラップインターフェイス」となる。
Windows 8のMetroが、「羊の皮をかぶったドブネズミ」でなければいいのだが。
#インターフェイスが「切り替わる」という点で、もうすでにアレ感が漂っている…
Windows 8はともかく、Windows Phone 7.5で「儲ける」のはまだ先かな。
でも、今のうちにかじっておくと幸せになれるかもしれない。アイデアがあれば!
#シェルと言えば、PowerShellを必要に迫られて多少弄ったが、どうしても「できそこない」感がぬぐえない…
#パイプラインが、テキストストリームから卒業した点は評価している。
#.NET Frameworkがもっと早く完成していて、同時にPowerShellがリリースされ、
#標準のシェルがPSになっていれば、もうちょっと印象がちがっていたかもしれない。
#でも、シェルコマンドのライブラリ作成がスマートじゃない。なんか異端な感じがするんだよな。
#よく頑張ったとは思うけどね。
#そういう印象は、Exchangeのコマンドレットを見ると特に強く感じる。
#PS使います・コマンドレットです、って言うだけで、応用性無いじゃん。
#あんな大量のコマンドレット、専門職じゃないし使えないって。バッチファイルと何が違うのさ?w
新しいVAIO Z
うーん。どうかなぁ。
そもそも高速GPU欲しい人が、「分離した」ノートを欲しいと思うだろうか。
それなら最初からパワフルなデスクトップと薄型ノートの方が使い出と自由度があるだろうし、出先でGPU欲しい人は結局GPU内臓ノートを買うのでは?
#光リンクでPCI Expressのブリッジが実現できたとか、技術的なチャレンジとしては
#面白いかもしれないけど。でも、「分離できます」よりも「内蔵できませんでした」
#に思えてしまう。
#売れるつもりで作ったんだよね? :-P
#いずれにしても、何とかタイマーを信じてるので、買うことはないのであった…
COMのアパートメント (4) スレッド親和性
(追記:2020/5/26)
COMについて、細々とですがこのページに誘導されてくるのが分かっています。もっとわかりやすく包括的に書いたページがありますが、残念ながら、なぜか検索エンジンのレートから外されているため、ここで案内しておきます。「ChalkTalk CLR – COMのすべて」 を最初に見ることをお勧めします。
前回までの内容で、COMが裏で何をしているのか、少し見えたような気がすると思う。
ここで重要な「嘘」を公表しなければならない(大げさか)。
今まで、ファサードが存在する理由としてスレッド同期を挙げたのだが、そのこと自体は間違っていないのだが、同期の対象が違っていた。
コンポーネントの「単一のインスタンス」について同期を行うために、ファサードを挿入すべきか否かという話だったのだが、実はそうではない。
本当のCOMでは、同期の対象は「ウインドウ」である(ほぼ)。
Win32 APIの生の呼び出しで、一からウインドウを生成するコードを書いたことがあれば、これは自明である。ウインドウにまつわるAPIの大半は、対象とするスレッドを限定する。
たとえば、Visual C++で、新しくWin32のプロジェクトを開始してみてほしい。ひな形として生成されるCのコード(微妙にC++だが)は、Win32 APIの直接呼出しの、懐かしい、目を背けたくなるコードだ。そのコードには、CreateThreadや、beginthreadexのような、スレッド生成のAPI呼び出しは存在しない。つまり、このコードは「シングルスレッド」で実現されている。
実行すると、単一の味気ないウインドウが表示される。一通りの操作が可能である。ウインドウが生成されると、「ウインドウハンドル」が返される。ウインドウハンドルの値は不透明値なのだが、user32ライブラリ内で非公開の構造体を指しており、この構造体はウインドウを生成したスレッドと結びついている(スレッドハンドルを保持しているのか、スレッドローカルストレージに関連付いているのか、詳細はわからない)。
そして、ウインドウメッセージは、例のメッセージ転送ループ(GetMessage→DispatchMessage)によって、これらのウインドウに転送されるのだが、このループはウインドウが関連付けられているスレッドで実行される(というよりも実行するようにコードを書く)。
プロジェクトテンプレートでは単一のメインスレッドを使うので、これらが全て同じスレッドで実行されるわけだ。
さて、たとえばウインドウにボタンがあったとして、ボタンクリックに反応するコードを書いたとしよう。「シングルスレッド」なので、ボタンクリックの処理中に別のメッセージが発生することはない。
DispatchMessageからウインドウプロシージャが呼び出され、ボタンクリックのお手製コードが走っている。そのため、この処理が終わって、次にGetMessageが呼び出されるまでは、新しいメッセージが目に見える形では現れない。
したがって、マルチスレッド実行による、複数のメッセージの同時発生と実行を考慮する必要はないわけだ。
これは鶏と卵の関係だが、Win32のウインドウメッセージ処理にこういう制限があるから、競合処理は発生しないとも言えるし、競合処理が発生しないようにWin32 APIが設計されているともいえる。
この制限が時には苦痛となる。時間がかかる処理を行う場合だ。そのような処理をボタンクリックの処理で実装すると、ウインドウは他の処理が実行出来ない。ウインドウのサイズ変更・移動・ウインドウの描画まで含めて、「固まった」かのようになる。
仕方がないので、ここでワーカースレッドを生成して、長く時間のかかる処理を委譲したとする。これで、長い処理を並行して実行できるので、ウインドウの基本的な動作に支障は出なくなる(ボタンが2回クリックされないように無効化するなどの工夫は必要だが)。
しかし、このワーカースレッドの処理が完了したときに、それをどのようにウインドウに通知するかと考えた時に問題が発生する。
大半のウインドウAPIが、特定のスレッド(この場合はウインドウを生成したのがメインスレッドなので、メインスレッド)から呼び出されないと機能しない、あるいは誤動作を起こす。そこで、PostThreadMessageなどを使って、メッセージキューに結果となるメッセージを送信する。
メッセージキューはそのうちメインスレッドのGetMessageによって引き出され、晴れて「メインスレッド」でメッセージが処理される。この特別なメッセージの処理コードを書いておけば、ワーカースレッドの処理結果は間接的にメインスレッドの実行中に処理され、ウインドウに何らかの表示を行ったりすることが出来るようになる。
.NET Frameworkにおいても、System.Windows.Forms.Control.Invoke()などを使って、背景では同じことを行う。
乱暴な言い方をすれば、クリティカルセクションがウインドウに紐付いていて、ウインドウを基準に同期を行っていることに等しい。
分かっている人には当たり前のことではあるが、この制限をスレッド親和性のモデルとしたのが、COMのアパートメントというスレッド属性だ。何故か? それはCOMの構造を設計したときに、それがスレッディングにまつわるWindows独自の問題だったからだ(多分)。
何らかのアプリケーションのソースコードが手元にあったとする。このソースコードはすでに規模が大きく、マルチスレッドに対してどこまで安全であるかが分からない。注意深く設計されていれば、スレッドの導入にも頑健であるだろうが、今までにファサード導入で見てきたように、スレッド親和性を担保するには相応のコストが必要だ。おまけに、ウインドウの制御に関しても、どのウインドウがどのスレッドに紐付いていて、どう安全でどう危険であるかも明らかではない。アプリケーションによっては、GetMessageとDispatchMessageによるメッセージループが「複数」の別々のスレッドで実行されている可能性すらある。
また、同じ問題が(一般的なCOMではない)コンポーネントライブラリにも存在する。このような状況下では、あるコンポーネントがあるアプリケーションやコンポーネントと協調して動作させるなど、効率以前の問題で、悪夢としか言いようがない。サードパーティ製のライブラリを結合するとき、スレッド親和性についてほとんどの場合は無視(つまり親和性などなく、ウインドウとスレッドの安全は自分で担保が必要と仮定)するはずだ。
幸い、COMは「インターフェイス境界」が非常にはっきりしている。そのため、この境界を基準として、ウインドウに紐付いた暗黙のスレッドとの競合についても、背後で面倒を見ようとしたわけだ。
つづく
COMのアパートメント (3) スレッドの属性とコンポーネント
さてと、アパートメントという用語がどのあたりから来るのか匂わせておいたところで、2つの軸をどのように対処するのかを見ていく。
一つ目は、ファサードの有無の選択だ。
main関数で例を示したように、このままではクラスの呼び分けをする必要がある。
そこで、newする実装を隠ぺいすることを考える。たとえば、以下のような関数を用意する。
IAdd *CreateInstance(const bool threadSafe) { IAdd *pInstance = new AddCalculator(); // スレッドセーフなコンポーネントが必要なら if (threadSafe == true) { return new AddCalculatorFacade(pInstance); } else { return pInstance; } }
呼び出す側は、スレッドセーフな実装が必要であればtrueを、そうでなければfalseを与えれば良い。しかし、これではクラスの型を指定するのが、boolの値にすり替わっただけだ。
そこで、いよいよコンポーネント自身にスレッド親和性の情報を付加する時がやってきた。
まず、コンポーネントの実装に、コンポーネント自身のスレッドセーフ属性を加える。これはコンポーネントのインスタンスが生成される前に参照する必要があるので、static関数とする。
class AddCalculator : public IAdd { public: // 途中省略 static bool IsThreadSafe() throw() { // このコンポーネントはスレッドセーフではない return false; } }; class AddCalculator2 : public IAdd { public: // 途中省略 static bool IsThreadSafe() throw() { // このコンポーネントはスレッドセーフだ return true; } }; IAdd *CreateInstance() { // コンポーネントのスレッドセーフ属性を得る const bool componentIsThreadSafe = AddCalculator::IsThreadSafe(); // const bool componentIsThreadSafe = AddCalculator2::IsThreadSafe(); IAdd *pInstance = new AddCalculator(); // コンポーネントがスレッドセーフでなければ if (componentIsThreadSafe == false) { // ファサードを挟んでスレッドセーフにする return new AddCalculatorFacade(pInstance); } else { return pInstance; } }
まだ納得いかないだろう。特にスレッドセーフ属性を取得するくだりは、コンポーネントの型が分かっているのだから、意味がないように見えるかもしれない。今はテンプレートを使ってお茶を濁しておく。テンプレート経由だが、コンポーネントが自身でスレッドセーフ属性を提供している事がわかると思う。
template <typename T> IAdd *CreateInstance() { // コンポーネントのスレッドセーフ属性を得る const bool componentIsThreadSafe = T::IsThreadSafe(); IAdd *pInstance = new T(); // コンポーネントがスレッドセーフでなければ if (componentIsThreadSafe == false) { // ファサードを挟んでスレッドセーフにする return new AddCalculatorFacade(pInstance); } else { return pInstance; } } int main() { // まだ実質的な変化はない... IAdd *pInstance = CreateInstance<AddCalculator>(); // IAdd *pInstance = CreateInstance<AddCalculator2>(); // 使う... delete pInstance; return 0; }
上のコードに意味が見いだせないのは当然で、今までやって来た事の形を変えただけだからだ。「ついに」、もう半分の肝心な部分を付け足す。それは、現在のスレッドの指標を加味することだ。
// スレッドローカルストレージ(スレッド毎に保存される変数) static __declspec(thread) bool g_MultiThreadBound = false; // 現在のスレッドにスレッド属性を設定する void SetThreadBound(const bool isMultiThreaded) throw() { g_MultiThreadBound = isMultiThreaded; } template <typename T> IAdd *CreateInstance() { // コンポーネントのスレッドセーフ属性を得る const bool componentIsThreadSafe = T::IsThreadSafe(); IAdd *pInstance = new T(); // 現在のスレッドがマルチスレッドで使用する前提でかつ、 // コンポーネントがスレッドセーフでなければ if ((g_MultiThreadBound == true) && (componentIsThreadSafe == false)) { // ファサードを挟んでスレッドセーフにする return new AddCalculatorFacade(pInstance); } // コンポーネントがスレッドセーフなら、現在のスレッドがどのような属性でも問題ない。 // コンポーネントがスレッドセーフでなくても、現在のスレッドがシングルスレッドでのみ使用する属性なら問題ない。 else { return pInstance; } }
さて、これで、コンポーネントを使用する側にとっては、スレッドの状態を表明さえすれば、そのコンポーネントがスレッドセーフだろうが、そうでなかろうが、安全に使用できるようになった。使用者はコンポーネントがスレッドに対して安全であるかどうかを考えなくてもよくなったという事だ。
シングルスレッドで使用するなら、
int main() { // シングルスレッドでのみ使用する場合 SetThreadBound(false); // どちらの実装を使ったとしても、最適なインスタンスが提供される。 IAdd *pInstance = CreateInstance<AddCalculator>(); // IAdd *pInstance = CreateInstance<AddCalculator2>(); // 使う... delete pInstance; return 0; }
マルチスレッドで使用するなら、
// スレッドのエントリポイント int ThreadEntryPoint() { // マルチスレッドで使用する場合 SetThreadBound(true); // どちらの実装を使ったとしても、最適なインスタンスが提供される。 IAdd *pInstance = CreateInstance<AddCalculator>(); // IAdd *pInstance = CreateInstance<AddCalculator2>(); // 使う... // (場合によっては別のスレッドにポインタを渡して使うかも) delete pInstance; return 0; }
なんとなく、普段COMでやっていることに近づいてきたのが分かるだろうか。
つづく。
最新のCOM記事:
COMについて、ローカルディスカッションで大幅に拡充してまとめ上げた記事があるので、そちらもどうぞ:「ChalkTalk CLR – COMのすべて」
COMのアパートメント (2) コンポーネントファサード
もうあとちょっと導入を。
前回のファサードクラスによる分離を進めてみる。
このファサードクラスは、AddCalculatorクラスを内包してしまっているため、インスタンスを外部から与えられるようにすると、柔軟性が増す。
class AddCalculatorFacade : IAdd { private: AddCalculator *pInner_; CRITICAL_SECTION cs_; public: AddCalculatorFacade(AddCalculator *pInner) throw() : pInner_(pInner) { ::InitializeCriticalSection(&cs_); } ~AddCalculatorFacade() throw() { delete pInner_; ::DeleteCrirticalSection(&cs_); } LONGLONG Add(const LONGLONG adder) throw() { ::EnterCriticalSection(&cs_); const LONGLONG result = pInner_->Add(adder); ::LeaveCriticalSection(&cs_); return result; } };
これならば、コンストラクタでインスタンスを指定することが出来る
(更にAddCalculator *を、IAdd *にすれば、もっと汎用的になる)。しつこいが、念のため例を示す。
int main(int argc, const char *argv[]) { IAdd *pAddInner = new AddCalculator(); // ここだけで選択(ファサードを挿入するかどうか) IAdd *pAdd = pAddInner; //IAdd *pAdd = new AddCalculatorFacade(pAddInner); const LONGLONG result = pAdd->Add(12345); delete pAdd; printf("Result=%I64dn", result); return 0; }
で、いよいよスレッド親和性の話になるのだが、以下の事が重要なポイントだ。
- あるコンポーネント(クラス)が、スレッドに対して安全かどうかを表明する。
- コンポーネントにアクセスしようとしているスレッドが、「安全なスレッド」かどうかを判別する。
ということが出来れば、コンポーネント毎にスレッドの安全性を「ファサード」を使って自動的に担保できるようになる。
あるコンポーネントは、最初のAddCalculatorのように、スレッドに対する安全性が無いとする。それを、単一のスレッドでしか実行しない場合は、ファサードが挿入されない。それによって、効率よくアクセスすることが出来る。
同じコンポーネントで、複数のスレッドで同時にアクセスされることがわかっている場合の、「個々のスレッド」に対して、自動的にファサードを挿入したポインタを渡す。
これで、メソッドの呼び出しが同期化されて安全にアクセスできる。
初めからスレッドセーフなコンポーネントAddCalculator2を、単一のスレッドでしか実行しない場合は、ファサードは挿入されない。
同じコンポーネントで、複数のスレッドで同時にアクセスされることがわかっている場合の、「個々のスレッド」に対してファサードは挿入されない。
なぜなら、元からAddCalculator2はスレッドセーフだとわかっているからだ。
このように、スレッドとコンポーネントの両方に、何らかの「指標」やら「属性」があれば、スレッドセーフを自動的に担保することが出来る。
そして、スレッド側に付ける「指標」は、「アパートメント」という名前の属性だ。
つづく。
最新のCOM記事:
COMについて、ローカルディスカッションで大幅に拡充してまとめ上げた記事があるので、そちらもどうぞ:「ChalkTalk CLR – COMのすべて」
COMのアパートメント (1) スレッド同期の隠蔽
(追記:2020/5/26)
COMについて、細々とですがこのページに誘導されてくるのが分かっています。もっとわかりやすく包括的に書いたページがありますが、残念ながら、なぜか検索エンジンのレートから外されているため、ここで案内しておきます。「ChalkTalk CLR – COMのすべて」 を最初に見ることをお勧めします。
COMのわかりにくい概念のひとつに「アパートメント」がある。
COMのほとんどの概念は、抽象性を突き詰めていった結果として生まれているため、このアパートメントも解説がなければ非常に難しい。
DonBoxが書いた「Essential COM」に詳しく書いてあるが、それでも頭をひねって考える必要がある。
同じくEssential COMの第一章では、COMのインターフェイスという概念が生まれた背景について、非常にうまく説明している。ここでは、同じような方法でアパートメントを説明してみることにする。
#導入は実は少々怪しいところがあるが、この方法でアパートメントを解説している文書は見たことがないので、お役にたてれば幸い。
非常に初期の、C言語が広まる機運が高まりつつあるころ、C言語でプログラムを書いていた人は、次のような疑問に遭遇した。つまり、いかにしてprintfやstrcpyのような独立性の高い、再利用可能な関数を書いて、それをライブラリ化するかということだ。
COMのインターフェイスはそこが基礎になっているのだが、これは言ってみれば「バイナリレベルのライブラリ(コンポーネント・クラス)の結合」を自由に行うための技術と言っていい。そのために、インターフェイス定義でコンポーネントの「顔」を抽象化して、綺麗さっぱりコンポーネントを分離できるようにしたというわけだ。
アパートメントはこれを更に推し進めて、「スレッド親和性」を抽象化したものだ。で、スレッド親和性って何?という話になるのだが、これを以下で説明する。
ここに、非常に単純な、足し算を行うCOMコンポーネントがある。本当にIDLから説明すると面倒なので、C++のクラスでこれを模式してみる。
class AddCalculator { private: LONGLONG value_; public: AddCalculator() throw() : value_(0) { } LONGLONG Add(const LONGLONG adder) throw() { value_ += adder; return value_; } };
Add()を呼び出すことによって、引数の値を足し算して結果を取得する。結果は戻り値として得られる。
このクラスは一般的な使い方で十分機能する。しかし、それは「シングルスレッド」や「マルチスレッド中の特定のスレッド」で使用した場合のみだ。
たとえば、Add()をマルチスレッド中の複数の異なるスレッドから同時にアクセスした場合、計算や結果の戻り値が容易に破たんすることがわかるだろう。
32ビットプラットフォームならさらにひどい結果となる(だからLONGLONGで例を書いてみた)。
問題は、Add()のブロック内の足し算を行っている個所、そして結果をreturnで返している部分。ここが別々のスレッドで同時に実行されると、競合状態が発生する。
そのため、マルチスレッドでも問題なくコードを実行できるようにするためには、ここを「排他制御」で保護する必要がある。
一般的にはクリティカルセクションを使う。コードを書き直してみる。
class AddCalculator2 { private: LONGLONG value_; CRITICAL_SECTION cs_; public: AddCalculator2() throw() : value_(0) { ::InitializeCriticalSection(&cs_); } ~AddCalculator2() throw() { ::DeleteCrirticalSection(&cs_); } LONGLONG Add(const LONGLONG adder) throw() { ::EnterCriticalSection(&cs_); value_ += adder; const LONGLONG result = value_; ::LeaveCriticalSection(&cs_); return result; } };
足し算の計算を行っている部分と、戻り値として返す値を確定させる部分(resultに代入する)をクリティカルセクションで保護した。
resultに代入しなおすのは、32ビット環境ではこの一行が2つかそれ以上のマシン語命令に置き換えられてしまい、そのコードを実行し終わるまでを保護する必要があるからだ。そして、たとえ64ビットであっても、return文の実行の直前にvalue_が書き換えられてしまうと、adderに指定した値以上の結果が返されてしまう可能性がある。それを保護するため、一旦resultに値を代入して、絶対に結果が変わらないようにするというわけだ。
さて、これでマルチスレッド環境で、複数のスレッドから同時にAddを呼び出しても問題なく動作するようになった。このAddCalculator2クラスは、すべてAddCalculatorクラスと置き換えることが出来る。つまり、マルチスレッド環境だけではなく、シングルスレッド環境でも問題ない。シングルスレッド環境ではクリティカルセクションで保護する意味はないが、保護していたとしても動作に影響はないからだ。
パフォーマンスの問題を除けば。
要するに、シングルスレッド環境で動かす場合、クリティカルセクションによる排他処理は完全に無駄になる。たとえば、上記のような単純な例でも1000万回呼び出されるとすれば、小さな排他制御のコードでさえ大きな無駄になる可能性がある。もちろん、コード全体からみて、この処理にどれだけの実行割合があるかによるのだが。
一つの方法としては、コンパイルオプションで、シングルスレッドの場合はAddCalculator、マルチスレッドの場合はAddCalculator2を使い分けるという方法がある。しかし、この方法には問題がある。
- 常に二つの似て異なるクラスを保守しなければならない。
- これらのクラスを使う側は、クラスのスレッドに関する安全性に気を配らなければならない。どちらのクラスがスレッドに対して安全なのか、そうではないのか、知っている必要がある。
- 実行時に切り替える事は出来ない。
実はこの方法は、すでになじみのある方法だ。VC++ではシングルスレッドとマルチスレッドのライブラリを選択出来る(出来た。新しいVCではシングルスレッドライブラリは廃止されている。廃止された理由は、やはり「保守」の問題ではないだろうか)。
シングルスレッドなプログラムのコンパイルにマルチスレッドのライブラリを使っても実害はないが、速度は遅くなる。
さて、もう少し前向きな方法を考えてみる。同期処理をファサードで分離してみてはどうだろうか。AddCalculatorクラスのファサードAddCalculatorFacadeを作る。
class AddCalculatorFacade { private: AddCalculator inner_; CRITICAL_SECTION cs_; public: AddCalculatorFacade() throw() { ::InitializeCriticalSection(&cs_); } ~AddCalculatorFacade() throw() { ::DeleteCrirticalSection(&cs_); } LONGLONG Add(const LONGLONG adder) throw() { ::EnterCriticalSection(&cs_); const LONGLONG result = inner_.Add(adder); ::LeaveCriticalSection(&cs_); return result; } };
これで、計算の実態はAddCalculatorのまま、スレッドセーフなクラスとしてAddCalculatorFacadeを使えばよくなった。計算ロジックの保守は、AddCalculatorクラスだけを行えばよい。マルチスレッドの安全性が欲しい場合は、AddCalculatorFacadeを使い、シングルスレッドで安全であることがわかっていればAddCalculatorを使って「最速」を手に入れることが出来る。
このままでは、コンパイルオプションで切り分けるという部分は未解決だ。そこで、共通のインターフェイス「IAdd」を定義して分離してみる。
class IAdd { public: virtual ~IAdd() throw() { } virtual LONGLONG Add(const LONGLONG adder) throw() = 0; }; class AddCalculator : public IAdd { private: LONGLONG value_; public: AddCalculator() throw() : value_(0) { } LONGLONG Add(const LONGLONG adder) throw() { value_ += adder; return value_; } }; class AddCalculatorFacade : IAdd { private: AddCalculator inner_; CRITICAL_SECTION cs_; public: AddCalculatorFacade() throw() { ::InitializeCriticalSection(&cs_); } ~AddCalculatorFacade() throw() { ::DeleteCrirticalSection(&cs_); } LONGLONG Add(const LONGLONG adder) throw() { ::EnterCriticalSection(&cs_); const LONGLONG result = inner_.Add(adder); ::LeaveCriticalSection(&cs_); return result; } };
これで、コードを使う実態は常にIAddインターフェイスを使ってアクセスし、クラスをnewするところだけで、AddCalculatorかAddCalculatorFacadeかの選択を行えばよい。
int main(int argc, const char *argv[]) { // ここだけで選択 IAdd *pAdd = new AddCalculator(); // IAdd *pAdd = new AddCalculatorFacade(); const LONGLONG result = pAdd->Add(12345); delete pAdd; printf("Result=%I64dn", result); return 0; }
さて、以上のようになっても、どちらのクラスを使用すべきか?という問題が残る。そして、シングルスレッドで使っていたクラスが、ある時から別のスレッドによってアクセスされるような場合に、この方法では対処出来ない。そのような場合、仕方がないから初めからマルチスレッド対応のAddCalculatorFacadeを使うという事になる。
これをどうやって解決するのかということの、頭をひねった結果が「アパートメント」という考え方だ。
つづく。
KeSetEventの第三引数
KeSetEventの第三引数について、「WDKプログラミング」のコラムにはTRUEを指定すべき理由は全くない、と言う事が書いてある。また、DDKの説明は混乱を招くとの事で、ここで詳しく動作を解説している。
が、この説明もカーネル同期の内部動作についてわかっている(あるいはなんとなくわかっている)ことが前提のような説明で、結局難解なままだ(翻訳が良くないのか?)。
ユーザーランドでの同期コードを書いていた人なら、イベントオブジェクトをセットして待つという動作を行うのに、SignalObjectAndWaitというAPIがある事を知っていると思う。カーネルAPIには対応するようなものが無いのだが、このKeSetEventの第三引数はまさにそれを実現するためにある。
KEVENT event; KeInitializeEvent(&event, SynchronizationEvent, FALSE); KeSetEvent(&event, IO_NO_INCREMENT, TRUE); assert(KeGetCurrentIrql() == SYNCH_LEVEL); LARGE_INTEGER timeout; timeout.QuadPart = -10000000; KeWaitForSingleObject(&anotherEvent, Executive, KernelMode, FALSE, &timeout);
KeSetEventの第三引数にTRUEを指定すると、APIから戻ってきたときにSYNCH_LEVELというかなり高いIRQLが維持される。これはそのままKeWaitForSingleObjectに引き継ぐことが可能で、この間同じCPUでのプリエンプションは絶対に発生しない。この組み合わせによって、SignalObjectAndWaitと同じことが実現できる。
なお、 SYNCH_LEVELで維持するのが妥当な部分はこの操作だけなので、当然待機しないでなにか別の作業を行うのはやめておいたほうが良い。実際のところ、こんな高いIRQLでは出来ることも限られてくるのだが…
また、はじめからDISPATCH_LEVELで実行すれば、同じことが実現できるような気がするのだが、DISPATCH_LEVELで時間を指定しての待機は「お手付き」なので、やはりこの方法しかない。
(多分TEBに、このモードを実行中でSYNCH_LEVELにしたというフラグか何かがあるのだろう)
いや、「ダサい」と思うよ。WindowsカーネルのAPIは、こういうダサさが至る所に漂っている。
#この本、深い部分を述べている点では良いのだが、どうも説明がわかりにくい。
#より古い「NT Driver」の方が理解しやすい。