COMのアパートメント (6) アパートメントの種類はどのように決まるのか

(追記:2020/5/26)

COMについて、細々とですがこのページに誘導されてくるのが分かっています。もっとわかりやすく包括的に書いたページがありますが、残念ながら、なぜか検索エンジンのレートから外されているため、ここで案内しておきます。「ChalkTalk CLR – COMのすべて」 を最初に見ることをお勧めします。


ずいぶんご無沙汰になってしまった。未だにCoInitializeExとかRPC_E_CHANGED_MODEで検索してくる方が絶えないので、少しだけ続きを書く。

前回、アパートメントの種類は、ライブラリの呼び出し元(正確にはスレッドを生成したコードの設計者)が知っているはずと述べた。だが、現実には、STAとMTAのどちらを使用すべきか、はっきり分かっていない開発者が多いと思う。
また、STAは重く、MTAにすれば軽いと、「まことしやかに」ささやかれていたりもする。もっと印象的なのは、「マルチスレッド」だからというものだ。

CoInitializeExでSTAかMTAを指定する事の他に、スレッドにアパートメントを指定する「意味づけ」がある。以下にこれらをまとめる。

  • STA – シングルスレッドアパートメント
    あるスレッドがCoInitializeExでSTAに設定した場合、そのスレッドはウインドウのメッセージループを持つと仮定する。つまり、そのスレッドが実行するコードの中核には、GetMessage・TranslateMessage・DispatchMessageによるメッセージループが存在する(しなければならない)。
    これは、極々一般的なWin32アプリケーションのメインスレッドの要件となる。同時に、例えワーカースレッドであっても、その中核がメッセージループで構成されているならば、STAとして設定する必要がある。
    もし、無理やりMTAとする場合、非常に難しい問題を自力で回避しなければならず(ウインドウメッセージとインターフェイス呼び出しの手動マーシャリング)、それが不可能な場合もある。通常はそのような事をする意味はない。
  • MTA – マルチスレッドアパートメント
    あるスレッドがCoInitializeExでMTAに設定した場合、そのスレッドにSTAのようなメッセージループの要件は不要となる。しかし、これは同時に、ウインドウメッセージとの連携は「全くない」と仮定する事と同じである。
    自分がウインドウメッセージと関連が無い(それどころか、ウインドウと関連が無い)と決めてかかるのは危険だ。そのスレッドが、あらゆるユーザーインターフェイスを、「直接的」ないし「間接的」に操作していないと確信できるなら、MTAを使用する事が出来る。これは、特に自分が書いていないサードパーティのコードを流用する場合は難しい。
  • メインSTA
    これはプロセスのメインスレッドがSTAに設定されている場合に、特にその状態を言う。コードの中には、メインスレッドでのみ操作可能な、特別な位置づけのAPIや変数などが存在する場合がある(つまり、例えSTAでも、ワーカースレッドからは操作してはならない、など)。これらを「メインSTAに紐づけられている」、と擬似的に考える。

スレッドにSTAを設定すると、スレッド毎に独立したSTAに所属する。つまり、スレッド=STAとなる。複数のスレッド(メインスレッドやワーカースレッド)が、それぞれCoInitializeExでSTAに設定されると、STAの部屋がそのスレッドの個数用意され、それぞれのスレッドがそれぞれのSTA部屋に入ることになる。

これに対して、スレッドをMTAに設定すると、プロセス当たりたった一つのMTA部屋に、MTA設定されたスレッドが同居する。STAはn個存在する可能性があるが、MTAは常に一つとなる。そして、STAのうちメインスレッドが入ったSTAにのみ、メインSTAという名前がついていると考えればよい。

で、これが何の役に立つのかと言う事だが、スレッド同士が会話したい場合(あるスレッドが、別のスレッドにあるCOMのインスタンスのメソッドを呼び出したりする)、この擬似的な「部屋」によって挙動が変わることになる。

Apartments

例えば、STAスレッド同士が会話する場合、相手のスレッドは別のSTA部屋に存在する(一つのスレッドには一つのSTA部屋が割り当てられるから、必ず別のSTA部屋になる)。すると、会話(メソッド呼び出し)は、ファサードとなるインターフェイスを経由しなければならない。COMの抽象化された世界では、メソッド呼び出しが直接成立しているように見えるが、実際には部屋が遠いので、秘書が電話でやりとりしているようなイメージだ。

物理的にはどうだろうか。それぞれのスレッドは両方ともウインドウメッセージのループを持っている。また、ウインドウメッセージの動作を邪魔すると困るので、お互いのスレッドに関与する場合はPostThreadMessageを使って、メッセージキューに内容を放り込むのが良いだろう(秘書に伝言する)。
時期的に都合がよくなる(ウインドウメッセージに同期)と、そのメッセージが取り出されて(秘書が社長に伝達)、「送り先のスレッド」で処理が実行される。返信も同様だ。元のスレッドのメッセージキューに結果が放り込まれ、元のスレッドの都合が良い時にとり出され、「送り元のスレッド」で処理が継続する。

この挙動が、一般的にWin32でワーカースレッドを生成して、時間のかかる処理をウインドウ処理から外だしした時と殆ど同じに見えるだろうか? 俯瞰して眺めるなら、STAとして設定することで、面倒なスレッド間メッセージングを隠ぺいして、RPCライクに呼び出し出来るようになると言う事だ。

だから、STAは「重い」のだ。

しかし、STAが重いからという理由だけでは、MTAに設定することは難しい。MTA同士の呼び出しでは、秘書も電話も存在しない。MTA部屋は一つしかない。その部屋の中で、社長が隣り合う席で直接話し合うようなイメージだ。ネイティブコードにおけるstd_callによって、直接メソッドが呼び出される。通常のプログラミングにおける「メソッド呼び出し」と完全に等価だ。それはすなわち、ウインドウメッセージやウインドウとの同期を一切行なわないと言う事だ。

仮にメッセージループを持つスレッドが、別のスレッドで生成されたCOMインスタンスのメソッドを直接呼び出した場合、メソッドは呼び出し元のスレッドで実行される。その結果、インスタンスが保持するウインドウハンドル(当然、別のスレッドに紐づいている)を、呼び出し元のスレッドが直接使用してしまうかもしれない。動作は予測不可能なものになる。

また、COMコンポーネントには、アパートメント属性の指定が無いものがある。このコンポーネントは、STAで生成される事を想定しているため、コンポーネント内でスレッド競合を特に考慮していない。MTAで生成すると、COMのインフラによって自動的にファサード(秘書通話に相当)が生成され、スレッド競合が回避される。

そのため、仮にだが、アパートメントを無視したような呼び出しを実行した場合(アパートメントを適当に設定すれば、結果的にそうなる可能性がある)、つまり、STAで生成されたこのようなインスタンスが、別のSTA/MTAからファサードの介在なく直接呼び出されたり、このインスタンスがコールバックすると、ウインドウメッセージの同期破綻はもちろん、ウインドウAPIの無効な呼び出し、スレッド競合による変数の破壊などの深刻な問題が発生する。

#以上の説明には、アパートメント設定の問題の他にも、プログラミング上の問題が含まれているが、そちらについては
#長くなるので、別の機会に。

この事を理解すれば、意味も分からずアパートメントを適当に設定するというのが、いかに危険か分かると思う。背景の理解は大変だが、判断の基準はシンプルで難しくない。

  • スレッドで行われる操作が、ウインドウやウインドウメッセージに及ぶ可能性がある場合(特にメッセージループを内包する場合)は、STAとして設定する。
  • それ以外であればMTAとすればパフォーマンスが向上する可能性があるが、心配ならSTAとして設定してもよい。
  • スレッドを生成したら、即アパートメントを設定する事。
  • COMインスタンスを操作する可能性が(直接的にも、間接的にも)完全にゼロである場合は、アパートメントを設定しなくて良い(CoInitializeExを呼び出す必要はない)。

.NETでコードを書いているのであれば、想像してほしい。Windows FormsならControl.Invoke、WPFならDispatcherを使いたくなる状況かどうかと同じ判断を、アパートメントに対しても行えば良い。これは、結局前回最後に書いた通り、これらのフレームワークのウィザードが生成するコードが「STAThread」とマークされ、メソッド呼び出しにマーシャリングを必要としていることに符丁するわけだ。

パフォーマンスが気になるというのなら、何が問題でSTAは遅いのか、と言う事を考えると答えが得られる。つまり、メソッド呼び出し(メッセージループにメッセージをポストしてやり取りする)にコストがかかることが問題なのであり、頻繁な呼び出しを避ければ良いのだ。プロパティを何度も参照したり、細かいメソッドを何度も呼び出したりすることを避けることで、一般的な使い方であればそうそう困ることはない。

COMコンポーネント側を設計するのであれば、頻繁なメソッド呼び出しを避けることができるように、拡張されたインターフェイスを設計しておくとよい。例えば、すべてのプロパティの値をまとめて取得・設定出来るようなメソッドや、バルク実行できるメソッド等が考えられる。

そして、COMインスタンスの操作を全く行わないのであれば、CoInitializeExを呼び出す必要はない。アパートメントの設定によって、STAならばウインドウメッセージキューが生成されてしまうし、MTAなら図中のスタックビルダーやインフラの準備でスレッドローカルストレージを消費するだろう。

例えば、素のWin32アプリケーションを作る場合、通常はCoInitializeExを呼び出したりはしない。しかし、ウインドウがActiveXコントロールを内包したり、WMI COMインターフェイスを使ったりするなど、直接的・間接的にCOMインスタンスを操作する可能性が生じれば、CoInitializeExでアパートメントを設定しなければならない。しかも、スレッドのエントリポイント(メインSTAなら、WinMain)の出来るだけ早い段階で、だ。

そして、ここが難しいところだが、アパートメントを設定しないのであれば、「間接的にも使われていない」と言う事を確認しておくことが重要だ(だから、心配ならSTAに設定する事をお勧めする)。

.NETのワーカースレッドで、スレッドのアパートメントを殆ど意識しないのは、COMインスタンスを.NETで操作する機会が(一般的には)あまり無いからだ。必要なければ、アパートメントを設定する必要もない。

猫でもわかるExpression Design

Windows Phoneアプリのアイコンを作るのに、VS2012ImageLibraryをいじって手を抜こうとしていたのだが、余計に面倒なことになってきたので、久しぶりにExpression Designを触った。某人からも「手ごろなお絵かきツールない?」と言われていたので、ここは一つチュートリアルを作らねばならないなと。


名称未設定1で、今日のお題はコレ→

分かってしまえば、「3分」で作れます。特にBlendを良く触る人は、「2分」で行けます。

用意するもの:
Microsoft Expression Design 4

3でも2でもあまり違いはありません。1は不明。Designはプロダクトから外れてしまったのですが、幸いなことに正式版が無償で公開されています!!

Microsoft Expression Design 4 (English)

このページは英語版ですが、下のほうに日本語版へのリンクがあるので、そこからダウンロードして下さい。すばらしい!


WS000000では、Designを起動します。

Designのファイルメニューの新規作成で、ドキュメントを作成します。

WS000001この新規作成ダイアログは、ドキュメントのサイズを指定できるようになっています。上の例では1024px×1024pxとしましたが、Designは「ベクター」ベースです。つまり、このサイズはあくまで指標であって、出力時にはスケーラブルに変更する事が出来ます。つまり、Designはペイント系ではなく、Illustratorのようなドロー系です。

ちなみに、Blend同様、数値入力ボックスの上でマウスをドラッグすると、手打ちしなくても値を上下出来ます。クリックすれば手打ちできます。これ良く考えてあるよね。但し、マウスボタンの調子が悪いと発狂するので、新調しましょう。


WS000002とりあえず、何も描かれていない状態です。

分かりにくいのですが、左下に「44%」と表示されています。これはキャンバスの拡大率です。もし、四角形がウインドウに収まっていないのであれば、ここを変更して丁度収まるようにして下さい。
(数値入力ボックスはドラッグできるんですよー?覚えてますか?)

WS000003まず、画角一杯の四角形を描きます。左のツールバーから、四角いアイコンをクリックし、出てきたサブメニューから「四角形」を選択します。

WS000004その後、キャンバスの左上隅から、右下隅までドラッグします。

キャンバスと同じ大きさなので分かりにくいですが、赤線の四角形が出来ました(赤線はこの図形が選択されていることを示します)。

WS000005やってみればわかりますが、キャンバスの四隅にはマウスポインタが「スナッピング」されるので、四隅の選択は非常に簡単です。ぎりぎりを狙って四苦八苦しなくても済みます。

WS000006ここで、右側のプロパティの「外観」に表示されている、色パレット(なんか綺麗にグラデーションしている面)で適当な色をクリックして選んでみて下さい。現在選択されているオブジェクト(つまり、今描いた四角形)の色が変わります。

WS000007もう色々触ってみたくなったカモ?w まぁ、ちょっと我慢して続きを。今回作るアイコンの背景はグラデーションさせたいので、実際にやってみます。ちょっと小さいのですが、基本パレット内のグラデーションパレット(左右に白黒グラデーションしているパレット)を選択します。

WS000008すると、色パレットの下にグラデーションバーが表示されます。ここも見た目と裏腹に非常に高機能なのですが、今回は簡単に2色のグラデーションをちゃっちゃと設定します。グラデーションバーの左側の下に、小さい四角のセレクタがあります。例では黒色ですね。これをクリックします。

WS000009すると、色パレットは、グラデーションバーのクリックした個所の色を指定出来るようになります。そこで、青っぽい色を選択します。この例のように、青が選択できない、青じゃない色しか出ていない場合は、すぐ右側にある虹色の縦のバーから青色付近をクリックすれば、選択できるようになります。

WS000010グラデーションバーの色の変化と同じように、四角形のグラデーションも変化したと思います。今度は右側の四角のセレクタ(白色)をクリックして、同様にちょっと濃さの違う青色を選択します。

WS000012背景完成!と行きたいところですが、グラデーションの方向が違いますね。色パレット右下の、トランスフォームアイコン(形が良く分からないので、スクリーンショットを参考に)をクリックし、回転角度をいじってください。

ここでは90°にしてみました。
(数値入力ボックスはドラッグできるんですよー!?覚えてますか!?)


WS000013これで、当初のもくろみ通りの背景が完成。次はルーペを作ります。ルーペの肝は何といってもレンズの部分でしょう。中抜きの円をどうやって描くかです。心配無用、超簡単。

ツールバーから、楕円を選択します。

そして、円を描くのですが、まずは中抜きの円の外形の大きさにします。四角形と同じようにドラッグで描けますが、そのままでは文字通り「楕円」になってしまいます。ここでは真円にしたいので、シフトキーを押しながらドラッグして下さい。XY軸が1:1の真円として描画されます。

もし、円までグラデーションになってしまったら、円を描いた時点で(つまり円が選択されている状態で)、右側の色パレットの基本パレットから白色を選択します。

WS000014そして、もう一つ円を描いてしまいましょう。コピペ(Ctrl-C、Ctrl-V)でも良いですし、新たに円を描いても良いです。

円の四隅に小さい四角があります。名称が良く分かりませんが、一般的には「アンカー」でしょう。このアンカーをドラッグして、中抜き円の内径に近づけます(シフトキーと併用ですよ!1:1になります)。

図形などのオブジェクトを操作する(移動や変形など)場合は、左側ツールバーの一番上の矢印アイコンを選択しておくと、やりやすくなります。

WS000015さらに円自体をドラッグして、最初の円と同心円付近に持ってきます。

WS000030ありがちなのは、外形が大きい円の「下」に小さい円がもぐりこんでしまって操作出来ないと言う場合です。WordとかExcelで図形を描画した事があるなら分かると思いますが、同じように修正できます。

円を右クリックして「整列」「背面移動」で、図形の順序を変更して下さい。

WS000016さて、準備が出来たら、大小2つの円を同時に選択します。シフトキーを押しながら、二つの図形をクリックします。両方とも赤線で囲われるはずです。

WS000017そして、「オブジェクト」メニューの「パス演算」、「背面マイナス前面」を選択します。

WS000031何が起こったか分かりますか?WPFで言うなら、GeometryCombineMode.Excludeですよー? 内径の大きさとして準備した図形で「引き算」を行ったわけです。これでルーペは出来たも同然。

ちなみに、このパス演算を使用すれば、三日月とか、どこかにありそうなマークとか、簡単に作れますね!
今回の場合、背景がグラデーションではなく単色であれば、内側の円の色を背景と同じにするだけでも行けますが、パス演算は強力なので、紹介しました。


WS000018後はルーペの柄の部分です。もう大体の操作方法は分かったと思いますが、一応やっておきます。まず、四角形ツールで長方形を描きます。

WS000033次にこの長方形を斜めに傾けます。長方形が選択されている状態で、下段中央付近に「回転角度」という数値入力ボックスがあります。これが正に選択しているオブジェクトの角度を表します。

例では(ドラッグして設定したので)45.1°になっていますが、手打ちすれば45°になりますよ。もうそろそろOK?

WS000021で、長方形の幅とか端処理をいじって、それらしくします。四角形の四隅を丸くするには、右のプロパティから「四角形の編集」「角の半径」という数値入力ボックスをいじります。ドラッグでぐわっと変えて、見ながら調節すれば良いでしょう。

WS000022いくぞー合体ー(ry

WS000023おぉ、完成だ! ….って、なんか変?

良く見ると、柄の長方形に縁取りが。

これは、オブジェクトの「ストローク」というやつです。図形を描くと、「ストローク(縁取り)」と「内面の塗りつぶし」の2つの属性が必要になります。今まで描いてきた四角形と円は、どちらも内面の塗りつぶし色だけを指定したため、縁取りがデフォルトのままになっているのです。

対処としては2通り考えられます。縁取りを塗りつぶしと同色にするか、縁取りを消すか、です。単純色なら同色でも問題ありませんが、今回のようにグラデーションを適用したり、テクスチャハッチングとかすると、模様が合わなくなって目立ってしまう可能性もあります。まぁ、そんな事に悩む頃には、どうすれば良いかも分かるでしょう。今は深入りせず、縁取りを消してしまいましょう。

WS000034図形を選択して、色パレットのタブを「ストローク」に変えます。

WS000035そして、基本色パレットから「なし」をクリックして、ストロークを無しにします。

これでOK。色パレット下のコンボボックスに「ストロークなし」と表示されていますね? ここをいじるとストロークの色だけじゃなく、模様とかテクスチャとか。まぁ、後で遊んでみて下さい。

ちなみに、塗りつぶしの色を「なし」にも出来ますよ。ストロークにのみ色を指定すれば、塗りつぶさない図形とか、簡単ですね?


WS000027さあ、完成です (^o^)/~~

WPFやっている人なら、ここまでの時点で「こりゃまんまWPFじゃないか」と思うかもしれません。たぶんそうです。というか、このDesignをWPFで実装しない理由はないでしょう。

WS000028さて、最後にこれをPNGで出力します。ファイルメニューの「エクスポート」で、フォーマットをPNG、画像サイズを256×256にして、パスやファイル名を指定してエクスポート。最初に書いたように、Designはベクターベース(WPFなら当たり前)なので、指定したサイズで綺麗に拡大・縮小されますよ。

WS000029ちなみに、PNGにすればベクターデータはすべて失われてしまいます。ちゃんとDesignのフォーマットでも保存しておきましょう(拡張子はdesign)。エクスポートではなく、普通に名前を付けて保存でOKです。

そして、PNGなら当然、透明色も反映されますよ。半透明や複雑な中抜き形状の図形を、アルファチャネルありで出力できます。図形や文字のアンチエイリアスも反映されます。


DesignとPaint.NETがあれば、とりあえずお絵かきに困ることはないと思いますよ。

ToggleSwitchを分離ストレージに関連付ける

WPFのトグルスイッチは、主に設定に使用する。
これが沢山あると、

ToggleSwitch

XAMLもそうだが、トグルイベントのハンドラもそれに合わせて書かなければならない。設定項目が増えるほど、このハンドラコードをチマチマと追加しなければならず、単純ミスも多くなる。
そこで、これをビヘイビアで何とかしようという話。

アプリケーションが複雑ではない場合、設定情報を単純に分離ストレージに格納すると思う。IsolatedStorageSettings.ApplicationSettingsコレクションを使用すると、出し入れは簡単になる。
ToggleSwitchの状態は、「オン」と「オフ」だけなので、ApplicationSettingsには単純にブール値で格納すれば良い。
そこで:

public sealed class AppSettingsBehavior : Behavior<ToggleSwitch>
{
    public static DependencyProperty SettingNameProperty =
        DependencyProperty.Register(
            "SettingName",
            typeof(string),
            typeof(AppSettingsBehavior),
            new PropertyMetadata(null));

    public static DependencyProperty OnTextProperty =
        DependencyProperty.Register(
            "OnText",
            typeof(string),
            typeof(AppSettingsBehavior),
            new PropertyMetadata("On"));

    public static DependencyProperty OffTextProperty =
        DependencyProperty.Register(
            "OffText",
            typeof(string),
            typeof(AppSettingsBehavior),
            new PropertyMetadata("Off"));

    public AppSettingsBehavior()
    {
    }

    public string SettingName
    {
        get
        {
            return (string)this.GetValue(SettingNameProperty);
        }
        set
        {
            this.SetValue(SettingNameProperty, value);
        }
    }

    public string OnText
    {
        get
        {
            return (string)this.GetValue(OnTextProperty);
        }
        set
        {
            this.SetValue(OnTextProperty, value);
        }
    }

    public string OffText
    {
        get
        {
            return (string)this.GetValue(OffTextProperty);
        }
        set
        {
            this.SetValue(OffTextProperty, value);
        }
    }

    protected override void OnAttached()
    {
        bool value;
        if (IsolatedStorageSettings.ApplicationSettings.TryGetValue(this.SettingName, out value) == false)
        {
            this.AssociatedObject.IsChecked = false;
        }
        else
        {
            this.AssociatedObject.IsChecked = value;
        }

        UpdateContent();

        this.AssociatedObject.Checked += AssociatedObject_Checked;
        this.AssociatedObject.Unchecked += AssociatedObject_Unchecked;
    }

    protected override void OnDetaching()
    {
        this.AssociatedObject.Checked -= AssociatedObject_Checked;
        this.AssociatedObject.Unchecked -= AssociatedObject_Unchecked;
    }

    private void UpdateContent()
    {
        this.AssociatedObject.Content =
            (this.AssociatedObject.IsChecked ?? false) ? this.OnText : this.OffText;
    }

    private void AssociatedObject_Checked(object sender, RoutedEventArgs e)
    {
        IsolatedStorageSettings.ApplicationSettings[this.SettingName] = true;
        UpdateContent();
    }

    private void AssociatedObject_Unchecked(object sender, RoutedEventArgs e)
    {
        IsolatedStorageSettings.ApplicationSettings[this.SettingName] = false;
        UpdateContent();
    }
}

というようなビヘイビアを作っておいて、

<ListBox Grid.Row="1" Margin="0,0,0,0" Padding="0,0,0,0"
    <ListBox.ItemContainerStyle>
        <Style TargetType="ListBoxItem"
            <Setter Property="HorizontalContentAlignment" Value="Stretch"/>
        </Style>
    </ListBox.ItemContainerStyle>

    <toolkit:ToggleSwitch Header="正規表現を使用する">
        <i:Interaction.Behaviors>
            <Behaviors:AppSettingsBehavior SettingName="IsRegexMode" OnText="オン" OffText="オフ" />
        </i:Interaction.Behaviors>
    </toolkit:ToggleSwitch>

    <toolkit:ToggleSwitch Header="大小文字を区別する">
        <i:Interaction.Behaviors>
            <Behaviors:AppSettingsBehavior SettingName="IsCaseSensitiveMode" OnText="オン" OffText="オフ" />
        </i:Interaction.Behaviors>
    </toolkit:ToggleSwitch>
</ListBox>

と書くだけで、次々とToggleSwitch項目を簡単に増やすことができる。SettingNameには、分離ストレージに格納するキーの名前を、OnTextとOffTextはスイッチ左側に表示される状態を示すテキストを指定する。これらは依存関係プロパティにしてあるので、バインディングで多国語化することもできる。

Metro UIのボタンエフェクトをWindows Phoneに適用する

Windows Phoneで遊び始めている。
Windows Phone自体もそうだが、WPFも殆ど触ったことがないので、とても苦しいw
取り合えず、簡単なアプリを実際に公開してみて感触をつかみつつ、WPF理解への足掛かりにしたいなと思っている。

で、早速製作中なのだが、Metro UI(名称がぽしゃったので、何と呼べばいいのか困るなぁ)に使用するユーザーインターフェイスのボタン、

WP8MetroUI

のマウスカーソルのあたりをタップした時に、ボタンの隅が押されて変形したような挙動でフィードバックがある、アレをやりたいと思ったのだが、どうも簡単に出来ないようだ。
で、WPFの変形の基礎とか、コントロールのカスタマイズの方法など、色々調べて以下のコードを書いた。

(ここまで紆余曲折の末、約2日 orz 直前にGeometry/Path/RenderTargetBitmapだけいじっていたのが幸いした。でないと、座標がdoubleというだけでも悶絶していたかもしれない…)

public sealed class TiltBehavior : Behavior&lt;UIElement&gt;
{
    private PlaneProjection projection_;

<pre><code>public TiltBehavior()
{
    this.Depth = 30.0;
    this.Tracking = true;
}

public double Depth
{
    get;
    set;
}

public bool Tracking
{
    get;
    set;
}

protected override void OnAttached()
{
    this.AssociatedObject.MouseLeftButtonDown += AssociatedObject_MouseLeftButtonDown;
    this.AssociatedObject.MouseLeftButtonUp += AssociatedObject_MouseLeftButtonUp;
    this.AssociatedObject.LostMouseCapture += AssociatedObject_LostMouseCapture;

    if (this.Tracking == true)
    {
        this.AssociatedObject.MouseMove += AssociatedObject_MouseMove;
    }
}

protected override void OnDetaching()
{
    this.AssociatedObject.MouseLeftButtonDown -= AssociatedObject_MouseLeftButtonDown;
    this.AssociatedObject.MouseLeftButtonUp -= AssociatedObject_MouseLeftButtonUp;
    this.AssociatedObject.LostMouseCapture -= AssociatedObject_LostMouseCapture;

    if (this.Tracking == true)
    {
        this.AssociatedObject.MouseMove -= AssociatedObject_MouseMove;
    }
}

private static void Apply(Size size, Point point, PlaneProjection projection, double depth)
{
    // コントロールのサイズからノーマライズした割合を得る
    var normalizePoint = new Point(
        point.X / size.Width,
        point.Y / size.Height);

    // 0~1の範囲外を切り捨てる
    var satulatePoint = new Point(
        (normalizePoint.X &amp;gt; 1.0) ? 1.0 : ((normalizePoint.X &amp;lt; 0.0) ? 0.0 : normalizePoint.X),
        (normalizePoint.Y &amp;gt; 1.0) ? 1.0 : ((normalizePoint.Y &amp;lt; 0.0) ? 0.0 : normalizePoint.Y));

    // 中心位置からの割合を得る
    var originPoint = new Point(
        satulatePoint.X * 2.0 - 1.0,
        satulatePoint.Y * 2.0 - 1.0);

    // 絶対位置
    var absolutePoint = new Point(
        Math.Abs(originPoint.X),
        Math.Abs(originPoint.Y));

    // 中心からの位置関係
    var directionX = originPoint.X &amp;gt;= 0.0;
    var directionY = originPoint.Y &amp;gt;= 0.0;

    // タップされた位置に応じて、回転軸位置を固定する(0又は1)
    projection.CenterOfRotationX = directionX ? 0.0 : 1.0;
    projection.CenterOfRotationY = directionY ? 0.0 : 1.0;

    // 辺ではなく、中心をタップした場合にも、フィードバックを得る
    // (辺をタップした場合は0に近づく事で影響を避ける)
    var distance = (absolutePoint.X &amp;gt; absolutePoint.Y) ? absolutePoint.X : absolutePoint.Y;
        projection.GlobalOffsetZ =
        (1.0 - distance) *
        0.5 *       // 中心位置でのZ座標
        (-depth);

    // Rotationは角度なので、計算して算出
    projection.RotationY =
        Math.Atan2(depth * (0.0 - originPoint.X) * 0.5, size.Width) /       // 0.5はGlobalOffsetZに含まれているので
        (Math.PI / 180.0);
    projection.RotationX =
        Math.Atan2(depth * originPoint.Y * 0.5, size.Height) /      // 0.5はGlobalOffsetZに含まれているので
        (Math.PI / 180.0);
}

private void AssociatedObject_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
    if (this.AssociatedObject.Projection == null)
    {
        this.AssociatedObject.CaptureMouse();

        projection_ = new PlaneProjection();
        this.AssociatedObject.Projection = projection_;

        var size = this.AssociatedObject.RenderSize;
        if ((size.Width * size.Height) &amp;gt; 0)
        {
            var point = e.GetPosition(this.AssociatedObject);
            Apply(size, point, projection_, this.Depth);
        }
    }
}

private void AssociatedObject_MouseMove(object sender, MouseEventArgs e)
{
    if (projection_ != null)
    {
        var size = this.AssociatedObject.RenderSize;
        if ((size.Width * size.Height) &amp;gt; 0)
        {
            var point = e.GetPosition(this.AssociatedObject);
            Apply(size, point, projection_, this.Depth);
        }
    }
}

private void Uncapture()
{
    if (object.ReferenceEquals(this.AssociatedObject.Projection, projection_) == true)
    {
        this.AssociatedObject.Projection = null;
        projection_ = null;

        this.AssociatedObject.ReleaseMouseCapture();
    }
}

private void AssociatedObject_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
    Uncapture();
}

private void AssociatedObject_LostMouseCapture(object sender, MouseEventArgs e)
{
    Uncapture();
}
</code></pre>

}

何しろWPFは完全に初心者なので、変なことをやっていたり、思想から外れる設計なのかもしれないのであしからず。これはUIElementクラスに適用できるビヘイビアクラスで、プロジェクトに入れておいて、ページのXAMLで以下のような感じで使う。

&lt;ListBox x:Name=&quot;MainListBox&quot; Margin=&quot;0,0,0,0&quot; Padding=&quot;0,0,0,0&quot;&gt;
    &lt;ListBox.ItemTemplate&gt;
        &lt;DataTemplate&gt;
            &lt;StackPanel Margin=&quot;8,0,8,8&quot;&gt;
                &lt;TextBlock Margin=&quot;0,0,0,0&quot; Padding=&quot;0,0,0,0&quot; Text=&quot;{Binding Name}&quot; TextWrapping=&quot;Wrap&quot; Style=&quot;{StaticResource PhoneTextSubtleStyle}&quot;/&gt;
                &lt;TextBlock Margin=&quot;8,8,0,8&quot; Padding=&quot;0,0,0,0&quot; Text=&quot;{Binding Description}&quot; TextWrapping=&quot;Wrap&quot; Style=&quot;{StaticResource PhoneTextSubtleStyle}&quot;/&gt;
                &lt;i:Interaction.Behaviors&gt;   &lt;!-- ココ --&gt;
                    &lt;Behaviors:TiltBehavior /&gt;
                &lt;/i:Interaction.Behaviors&gt;
            &lt;/StackPanel&gt;
        &lt;/DataTemplate&gt;
    &lt;/ListBox.ItemTemplate&gt;
&lt;/ListBox&gt;

ListBoxにコレクションをバインディングし、その要素毎にStackPanelで表示する。そのStackPanelにビヘイビアの指定を行うと、対応するクラスのビヘイビアが呼び出される。名前空間「i」は、System.Windows.Interactivityで、これはWindows Phone以外ではアセンブリが違うかもしれないが存在すると思う。最初にxmlns:iで宣言しておくこと。

なお、Buttonに適用するとうまく動かない。何故かは、これから悩むところ (^^;; StackPanelのクリックを検出する方向で逃げたほうがいいのか、Buttonで真面目にやるほうがいいのか、それすら分からないのが困ったものだ…

ThinkPad X201sで3G通信する

ThinkPad X201s

ThinkPadのよいところは、(保証に目をつむれば)カスタマイズの範囲が広い事だが、X系列は3G通信(WWAN)のオプションが搭載できる点でも良い。しかし、国内向けにはWWANオプションの選択肢が少ないか、あるいは提供されていない。

そんなわけで、どうすればそのようなX201sに3G通信の能力を与えられるか、というメモ。
(トライする場合は自己責任で、念のため)

  • QualComm Gobi2000 Mini PCIex moduleを買う。
    注意点は、ThinkPadの場合モジュールロックがあるので、ThinkPad用として販売されているモジュールしか使えない。例えば、Gobi2000はQualComm(Sierra)純正のモジュールが存在するが、これを取り付けるとBIOSがエラーを発してしまう。mod BIOSを入れるという選択肢もあるが、リスクを承知していない普通の人は手を出すべきではないだろう。特に純正モジュールは安かったりするので、飛びつかないように注意。
    もう一点は通信方式で、EVDO/EDGEタイプを買ってはいけない。国内の場合、DoCoMoかSoftBankに接続しなければならない。KDDIはEVDOの周波数帯は一致しているものの、KDDI側に機器の登録が必要らしいので、事実上使えない。
    今なら、Gobi3000やGobi4000(LTE)という選択肢もある。
    なお、これらモジュールは、技適マークがついている筈。
  • WWAN用無線アンテナを買う。
    国内向けThinkPadには、殆どのモデルでWWANアンテナが内蔵されていない。そのため、機種にあったWWANアンテナを入手する必要がある。ここで問題になるのが、アンテナの種類で、ヤフオクで売っているアンテナの大半は、GPSに対応していない。GobiシリーズはGPSにも対応しているのだが、こういったアンテナを買うと、WWAN通信は出来るがGPSは電波を拾えない。そんなわけで、GPSも使えるようにしたいのなら、Tycoのアンテナは多分ダメで、可能なら純正アンテナを入手するのが良い。
    X201sについての補足として、高解像度LCDを使っている場合は、そもそもWWANオプションは(世界的にも)存在しない。どうも天板の形状が通常LCDモデルと異なるようなので、仮に純正のアンテナを買うと、高解像度LCDモデルには取り付けできない可能性がある。
    なお、Gobi2000の場合は、アンテナは2系統必要。Gobi4000は、写真を見る限りでは3系統必要なようだ。私は、天板を外すのが面倒なので、3系統入れてしまった。Tycoのアンテナなので、GPSは使えない。取り付け位置は(幸いTycoは小型なので)適当に張り付けた。
  • SIMを入手する。
    国内では徐々に選択肢が増えてきた。手元で確認したのは、
    b-mobile ZSIM(使用しなければ0円のお手軽契約)
    DTI ServersMan SIM 3G 100(オプションなしで500円、100kbps)
    で、どちらもOKだった。ちなみに両方とも回線はDoCoMoを使う。

あとはドライバだが、Lenovo USA他の海外サイトから入手可能。変なドライバサイトからダウンロードしなければトラブルはないはず。これらはThinkVantageアプリケーションシリーズとして扱われており、Access Connection、GPSがある。但し、新品SIMのアクティベーションには以下のドライバが必要だが、この事はあまり書かれていないので注意(入れるだけでOK)。

Lenovo Mobile Broadband Activation (Windowsのバージョンが違う場合は探す事)

3GSIMを素で運用する場合、初めての場合は知らないかもしれないが、APNと呼ばれる識別子を設定する必要がある。b-mobileもDTIも説明書に書いてあるので、それに従ってAccess Connectionsのプロファイルを作ればよい。ユーザー名・パスワードも固定だが、忘れずに指定するように。

ACSettings1

ACSettings2

ACSettings3

晴れて接続できれば、以下のようになる。

Access Connection

コーディングガイドラインの車輪を再発明

正直、「好きだね、決めるのが」というところなのだが、コーディングガイドラインについての定義が多いなあと思う。
先日、CodeProjectで、またもやその一つが投稿され、コメントが炎上(?)していた。

Some practices to write better C#/.NET code

この中に何点か、自分的に納得いかないプラクティスがある。

  • Capitalization Conventions – プロパティはともかく、フィールドはプライベートもPascalなのか?自分的にはこうだ:moneyRemains_ – これは、ANSI C/C++において、安全とみなせるシンボル表記だ。但し、アンダースコアが後ろに続く表記はあまり見たことが無いかもしれない。自分のように、C++、C#のどちらも頻繁に書く場合、どちらでも通用する書き方にしたい。但し、それはシンボルが公開インターフェイスでなければ、の話だ。この記事ではないが、進行中のプロジェクトでは、readonlyフィールドが全て大文字という縛りがあって、かなり違和感がある。ここだけ単語の区切りが分かりにくいので、アンダースコアで区切ることになる(DIEING_DICTIONARYのような)。すると、確かに目立つは目立つが、まるでconstかと主張しているようだ(constではなくreadonlyなのだが)。どうも、readonlyインスタンスとconstの決定的な違いを分からず決めたように感じる。
  • DO declare all member variables at the top of a class, with static variables at the very top. – たのむからプロパティをコンストラクタより上に持ってこないでくれ。フィールドとプロパティを同一のように扱うと罠にはまる事があるから、見てすぐにわかるようにしておきたい。
  • DO NOT suffix enum names with Enum – これは同意だが、できれば列挙型名は複数形にして欲しいな。列挙型を定義してTlbExpでタイプライブラリを出力、C++側でインポートした時とか、不自由しなくなる。まれなケースだけど、まれなケースの時だけルールを変えるぐらいなら、複数形にする事に負担があるわけじゃないからやればいい。
  • Avoid Obsolete Comments – これはかなり炎上していたな。前提としては、コメントを「書かなくても済むようにする(シンプルで意味が明瞭な実装を心がける)」ということに賛成だが、不可避な場合がある。結局、突き詰めていくと、コーディングの問題じゃなくて、大枠の設計(ユーセージモデルとか)が問題で複雑化してしまっていたりするので、どうしてもコードが複雑にならざるを得ない事がある。そのような場合に、将来自分でこのコードを見たときに、昔自分が何を考えてこのように実装したかを書いておくためにコメントを使っている。これを書いておかないと(自分でも)理解不能ではないだろうかと思うような場所で、かつ改善できないような痛い場所だ。しかし、出来れば設計からやり直させたいね。
  • Avoid Multiple Exit Points – これは絶対拒否だ。一体これをやりたがるのは何故だ?CPUにはプログラムカウンタ(インストラクションポインタ)とスタックというものがあり、これらの情報で一種の仮想的な「ハードウェアステートマシン」を実現しているのだ。条件分岐のブロック内に居るという「事実」が、この情報だけで表現されているのに、何故手でステートを管理したがるのか。ステート変数を導入すれば、それがいつどこでどのように変更され、どのような場所や場合においても正しくふるまっているかどうかを管理しなければならなくなる。C++でRAIIが当たり前に使われているのは、こうしたステート変数を避けるためだ。これ以上、処理の必要がない分岐ルートでは、速やかにメソッドから退出(return)する。そうすることで、余計な分岐ルートを削減できる。分岐ルートを削減できれば、パターン網羅のためのテストコードも少なくて済む。メソッドが下に落ちていくほどルートはふるいにかけられ、終盤ではそこに居るための「暗黙条件」は絞られるはずだ。結果として、コードはよりシンプルになる。

思うに、こういう事は意外かもしれないが、「ルールを決めて徹底させる」事が諸悪の元凶に思う。たとえば、重厚な開発プロセスを導入する事は、そこから緩慢な死へと向かい、より良い未来に前進することはない。プロセスを定義するという事は、「考えなくても出来る」ことを目指すため、例外なく「思考停止」し、本当は何が問題でどうしようとしていたのかが置き去りにされてしまう。常に最善とは何かを考えるためには、プロセスは邪魔なのだ。

もっとも、多人数で作業を進めるために、何らかの決まり事は必要となる。重要なのは、それが常に陳腐化する可能性があり、いつでもすぐに見直し、正しい方向に修正しなければならないことをメンバー全員が周知していなければならない事だ。加えて、業務を遂行するために、いつでもルールが変更可能でなければならない。何らかのプロセスを導入していると、「こうするのがルールだから仕方ないよね」というところから始まって、なし崩し的に開発効率が落ちていく。終いには、何が悪くてこうなってしまったのかすら、誰もわからないという寒い状況に陥る。

上記に示したルールは、私が業務を進めるにあたって、C++/C#の両刀使いである前提でのルールも含まれている。CodeProjectの元記事や私のルールを鵜呑みにせず、積極的にルールをぶち壊し、再発明してほしいものだ。

Windows Server 2012 HYPER-V の嬉しい機能

ここ一週間、Windows Server 2008R2 のサーバーがディスク障害で逝かれて大変だった。

RAID1+毎日バックアップでハードウェア障害の問題は無いのだが、問題はバックアップの内容だった。HYPER-VのゲストOSに、物理ディスクを直接接続していると、保存状態(バックアップ時にVSSがVMを保存状態にする)のVMを復元できなくなる。

もちろん、完全に同一のハードウェアを用意すれば復元できるのだろうが、障害時には決まってそういう事が期待できないものだ。結局、個々のファイルとしてVMイメージは復元できるのだが、どうやっても新しい環境では認識させる事が出来ず、DCはお亡くなりになった(DCって言っても自分だけで使っているような物だからまだ良いのだが)。

たまたま、業務にクリティカルなVMはそういうしがらみが無かったので復元出来たので、駄目だったVMは完全に放棄し、Windows8の事もあって、HYPER-Vホスト・DC等を2012にアップグレードした。

で、こんなことは二度と御免なので、耐障害性を考えてVMを再編することにしたのだが、調べているうちに、2012のHYPER-VはエクスポートしていないVMイメージもインポート可能になったそうで、感涙やらショックやら。2008R2で出来ていれば!!!

Virtual Server 2005やVirtual PCでは出来てたんだよな。ファイルコピーのバックアップが。

これで、安心してWindows Serverバックアップで「ベアメタル回復」が使えそうだ。これの検証もしておく必要があるな。

LINQは本当に強力だ (8) 何が並列化されるのか

並列化が簡単に出来る事がLINQのアドバンテージの一つなのだが、AsParallelを付ける際に、クエリのどこがどのように並列化されるのかが見えているだろうか?
AsParallelを付ければ並列化出来るというのは簡単すぎるため、意味のない適用や実害が生じる可能性もある。たとえば、以下のコードを考えてみる。

// 重複しない乱数列を生成する
var rand = new Random();
var captured = new ConcurrentDictionary<int, int>();
var results =
    from index in
        Enumerable.Range(0, 100000)
    let value = rand.Next(200000)
    where captured.TryAdd(value, index) // 辞書に値の追加を試み、成功すればtrue
    select value;

少し脱線するが、ConcurrentDictionaryはDictionaryのスレッドセーフバージョンだ。しかも、ロック競合が起きにくく、かつアトミック操作が可能なメソッドが追加されており、上記のようにLINQでも使いやすい(DictionaryクラスはTryGetValueがoutパラメータを使うので、LINQでは使いにくかった)。

さて、このコードは小さいので並列化にはあまり向いていないが、それは横に置いておき、並列化したいのはどこなのかを考える。
やろうとしていることは、10万回分の乱数を取得し、その中から重複する値を避けた乱数列の取得だ。もちろん、Distinctを使えばいいのだが、それも横に置いておく。
生成した乱数(value)を辞書に追加し、成功すれば(つまり辞書にその値が無く、追加された場合)に、その値がselectで射影される。この、乱数の生成と辞書への追加の試行部分を並列化出来れば、大半の処理が並列化出来たことになる。
クエリ構文で書いていることもあり、AsParallelを挿入すべき場所は一か所しかないが、取りあえず挿入する。

var rand = new Random();
var captured = new ConcurrentDictionary<int, int>();
var results =
    from index in
        Enumerable.Range(0, 100000).
        AsParallel()    // 並列化
    let value = rand.Next(200000)
    where captured.TryAdd(value, index)
    select value;

LINQ(とPLINQ)を書き始めて間もないと、このAsParallelによって何が並列化されるのか、誤解する事が多いようだ。上記の例では、10万までの数値を生成する「Range」が並列化され、それ以降の乱数生成や判定が並列化されることも期待しつつも、本当に並列化されるのか自信が持てないらしい。
(いや、自分も最初はそうだったから、そういうものだと思う :-)

最初に書いた通り、良くも悪くもAsParallelを付けるだけでよいというのが、この誤解の主原因なのだろう。
まず、クエリ構文をメソッド構文にする。

var rand = new Random();
var captured = new ConcurrentDictionary<int, int>();
var results =
    Enumerable.Range(0, 100000).
    AsParallel().
    Select(delegate(index)
        {
            return new
            {
                value = rand.Next(200000),
                index = index
            };
        }).
    Where(delegate(entry)
        {
            return captured.TryAdd(entry.value, entry.index)
        }).
    Select(delegate(entry)
        {
            return entry.value;
        });

意味もなくletを使ってしまったので面倒な事になっているが、前回見せたとおりindexとvalueを射影しているだけだ。肝心のAsParallelは、letを射影するSelectの手前に入っている。Range→AsParallel→Select→Where→Selectと、パイプライン結合されているのが分かる。

そう、パイプライン結合されているのだ、AsParallelも。

パイプライン結合を実現しているのは、IEnumerable<T>インターフェイスだ。クエリの末端(最後のSelect)が列挙されるとき、GetEnumeratorが呼び出される連鎖が起きることを述べた。AsParallelもまた、同じようにGetEnumeratorの呼び出しと、列挙の連鎖が発生する。ということは、AsParallelが動作に影響を与えられるのは、自分よりも上位のRangeだけ、と言う事になる。

え?そうなの?
いやいや、実はこれが判りにくい原因ではないかと思う。AsParallelよりも下位のパイプライン結合が並列化されるのだ。つまり、乱数の生成と辞書への追加が、期待通り並列化される。Rangeによるindexの生成は並列化「されない」。
どうしてこのようになるのだろうか?ここにクエリ構文を使いすぎたり、varを使いすぎたりする弊害がある。上記のコードをばらばらにし、.NET2.0的にしてみる。

IEnumerable<int> results0 =
    Enumerable.Range(0, 100000);

ParallelQuery<int> results1 =
    results0.AsParallel();

ParallelQuery<Tuple<int, int>> results2 =
    results1.Select(delegate(index)
        {
            return Tuple.Create(rand.Next(200000), index);
        });

ParallelQuery<Tuple<int, int>> results3 =
    results2.Where(delegate(entry)
        {
            return captured.TryAdd(entry.Item1, entry.Item2);
        });

ParallelQuery results4 =
    results3.Select(delegate(entry)
        {
            return entry.Item1;
        });

匿名クラスは表現できないので、Tupleに置き換えてある。AsParallelの戻り値の型は、実はIEnumerable<T>ではない。ParallelQuery<T>型なのだ。但し、ParallelQuery<T>はIEnumerable<T>を実装しているので、これを直接foreachなどで列挙することは可能だ。つまり、今まで通りIEnumerable<T>が返されると思っていても、表面上の違いは無いということだ。

しかし、このコードを見れば、なんとなく並列化される範囲が見えてくると思う。ParallelQueryによって管理されているクエリが並列化される。少し不思議なのは、IEnumerable<T>に対してSelectやWhereを呼び出した場合はIEnumerable<T>が返されるのに、ParallelQuery<T>に対してSelectやWhereを呼び出した場合は、ParallelQuery<T>が返されることだ。

これもそれほど大げさな仕掛けではない。IEnumerable<T>の場合は、SelectやWhereといった拡張メソッドは「Enumerable」クラスに定義されている。ParallelQuery<T>に対してSelect・Whereした場合は、「ParallelEnumerable」クラスの拡張メソッドが使用されるのだ。C#のコンパイラが、SelectやWhereメソッドの第一引数の型にもっとも一致する拡張メソッドを自動的に選択するため、この仕掛けが機能する。まるで、メソッドのオーバーライドを実装しているかのようだ。

では、いったいどこで並列実行のからくりが実現されるのだろうか? ParallelQuery(ParallelEnumerable)の実装は複雑だが、基本的な考え方は単純だ。ParallelQuery<T>の最下位でGetEnumeratorが呼び出されたときに、並列化が行われる。

IEnumerable<int> results0 =
    Enumerable.Range(0, 100000);

ParallelQuery<int> results1 =
    results0.AsParallel();

ParallelQuery<Tuple<int, int>> results2 =
    results1.Select(delegate(index)
        {
            return Tuple.Create(rand.Next(200000), index);
        });

ParallelQuery<Tuple<int, int>> results3 =
    results2.Where(delegate(entry)
        {
            return captured.TryAdd(entry.Item1, entry.Item2);
        });

ParallelQuery<int> results4 =
    results3.Select(delegate(entry)
        {
            return entry.Item1;
        });

// ParallelQuery.GetEnumerator()が呼び出される
foreach (var value in results4)
{
    Console.WriteLine(value);
}

最下位でGetEnumeratorが呼び出されると、スレッドプールからいくらかのスレッドを割り当て、各スレッドがParallelEnumerableの拡張メソッドで定義された演算を実行する。この部分は、従来のGetEnumeratorによる結合では実行できない。何故なら、IEnumeratorインターフェイスはマルチスレッドに対応していないからだ。必然的に、パイプラインで並列化演算が可能なのは、それぞれの演算が専用に設計された、ParallelEnumerableに定義された拡張メソッド群だけ、ということになる。

(もちろん、それらの拡張メソッドから呼び出されるデリゲートの実装は、いくらでも拡張可能だ。SelectやWhereのデリゲートの実装は、使う側が記述するのだから。)


AsParallelの実装とか、ParallelQueryEnumeratorの実装に興味がわくかもしれない。この部分はスレッドの割り当てやデータの分散と集約など、実際にはかなり複雑になっていると思われる。

しかし、注目する点はその部分ではなく :-) 、各ワーカースレッドが並列実行している演算の部分だ。Select→Where→Selectの部分が、スレッド毎に分散されている。パイプラインでAsParallelを適用してから、foreachで列挙されるまでに結合された演算子が並列実行される事が分かる。
そして、(当たり前ではあるが)foreachによるGetEnumeratorの呼び出しの後(foreachループ)は、並列化されていない。Console.WriteLineが並列実行されるわけではない事も分かる。ここが並列実行されたとすると、記述したとおりに実行されないわけだから、C#の構文的にも変だ。
また、AsParallelの上位も並列化されない。こちらも、IEnumeratorインターフェイスの構造に阻まれて、並列化させることはできない。

結局のところ、IEnumerable.GetEnumeratorの呼び出しによって、並列化の「壁」が出来上がるわけだ。私はこれを「ゲート」と勝手に呼んでいる。PLINQクエリが入り混じる場合、このゲートを意図的に作ってやることで、並列化されるクエリの範囲を自由にコントロールできる。

IEnumerable.GetEnumeratorを明示的に呼び出させる事が出来れば、このゲートを作ったことになる。つまり:

IEnumerable<int> results0 =
    Enumerable.Range(0, 100000);

ParallelQuery<int> results1 =
    results0.AsParallel();

ParallelQuery<Tuple<int, int>> results2 =
    results1.Select(delegate(index)
        {
            return Tuple.Create(rand.Next(200000), index);
        });

// results2の直後にゲート生成
IEnumerable<Tuple<int, int>> results3 =
    ((IEnumerable<Tuple<int, int>>)results2).Where(delegate(entry)
        {
            return captured.TryAdd(entry.Item1, entry.Item2);
        });

IEnumerable<int> results4 =
    results3.Select(delegate(entry)
        {
            return entry.Item1;
        });

ということだ。こうすれば、results2だけが並列化の対象となる。

で、キャストは面倒であるため、この目的のための「AsEnumerable」という拡張メソッドがある。内部の実装はキャストしているだけだ。PLINQ向けには「AsSequential」もあるが、これはAsEnumerableと全く変わらない。AsParallelの逆演算子のイメージで導入されたのだと思う。

LINQは本当に強力だ (7) 範囲変数と構文の選択

LINQのクエリ構文には、範囲変数と呼ばれている機能がある。
たとえば、文字列で与えられた半径と円周を対にしてみる。

string[] values = new string[] { "234.56", "456.78", "345.67", "123.45" };
IEnumerable<Tuple<double, double>> results =
    from value in values
    select Tuple.Create(double.Parse(value), double.Parse(value) * 2 * Math.PI);

これで正しい結果が得られるのだが、文字列の数値をdoubleにパースする操作は2回行われる。
CLRがとても賢くなったとしたら、double.Parseに副作用が存在しないことをJITで確認して、1回の呼び出しに変換するかもしれないが、あまり期待できない。
そこで、普通ならパースした結果をローカル変数に入れておき、使いまわしたりするのだが、このような目的のために、LINQには「範囲変数」が存在する。

string[] values = new string[] { "234.56", "456.78", "345.67", "123.45" };
IEnumerable<Tuple<double, double>> results =
    from value in values
    let parsed = double.Parse(value)    // 範囲変数parsedに代入
    select Tuple.Create(parsed, parsed * 2 * Math.PI);

これなら確実に1回だけパースを行う。その結果を、範囲変数”parsed”に代入し、後で何度でも使いまわす事が出来る。
そういう訳で、このletは実に使い出がある。というより、これが無いとかなり困ったことになる。
ここで、新たな「let」という予約語(厳密には予約語ではないが、ここでは詳しく述べない)が登場するのだが、最初に見たときは「何でvarにしなかったんだ?」と思った。

もはやvarについて知らない人はいないと思うが、

// 右辺から、文字列の配列って分かるよね?
var values = new string[] { "234.56", "456.78", "345.67", "123.45" };
// 右辺から、Tuple<double, double>の列挙子って分かるよね?
var results =
    from value in values
    let parsed = double.Parse(value)
    select Tuple.Create(parsed, parsed * 2 * Math.PI);

というように書き直せる。右辺の結果の型が明確であれば、左辺にわざわざ型名を書かなくても良いでしょうというのが一点。もう一点は連載でもちらりと述べたが、匿名クラスをローカル変数に保持するのに、型名が書けないからという理由だ。

さて、やっとvarについて書けた。以後は注目すべき点がない場合はvarで書く :-)
varの意味づけがイメージ出来るように、実際に匿名クラスを使ってみようか。

var values = new string[] { "234.56", "456.78", "345.67", "123.45" };
// 型名が無いから、書きようがない
var results =
    from value in values
    let parsed = double.Parse(value)
    select new  // 匿名クラスだから、型名はない
    {
        R = parsed,
        Length = parsed * 2 * Math.PI
    };

上記の「results」は、varでしか宣言出来ない。故にvarという予約語が必要になった訳だ。
で、範囲変数であるletも事情は同じだ。場合によっては、型名が書けない事がある。それならば、letという予約語を作るよりもvarが使えたほうが、覚える予約語が減って良いのではないか?

var values = new string[] { "234.56", "456.78", "345.67", "123.45" };
var results =
    from value in values
    var parsed = double.Parse(value)    // varでいいじゃん?(もちろんNG)
    select new
    {
        R = parsed,
        Length = parsed * 2 * Math.PI
    };

何故letが導入されたのかは明らかではないが、私が感じた理由を示そうと思う。
まず、上のコードのアウトプットを少し増やす。

var values = new string[] { "234.56", "456.78", "345.67", "123.45" };
var rand = new Random();
var results =
    from value in values
    let parsed = double.Parse(value)
    let randomValue = rand.Next((int)parsed)    // 乱数を生成する
    select new
    {
        R = parsed,
        Length = parsed * 2 * Math.PI,
        Rand = randomValue
    };

ちょっと強引な例だが、パースした数値の範囲の乱数を加えた。見ての通り、あらかじめ代入した範囲変数parsedを使って、別の範囲変数を生成することも出来る。この辺りは、通常のローカル変数の感覚と全く変わらない。次のようなコードを書くまでは:

var values = new string[] { "234.56", "456.78", "345.67", "123.45" };
var rand = new Random();
var results =
    from value in values
    let parsed = double.Parse(value)
    let randomValue = rand.Next((int)parsed)
    orderby parsed  // ちょっとソートしてみようか。
    select new
    {
        R = parsed,
        Length = parsed * 2 * Math.PI,
        Rand = randomValue
    };

私は、上記のselectまで書いたところで、「凍り付いた」。意味が分かるだろうか?
parsedの値で昇順にソートするために、orderbyを書いた。ここまでは良いのだが、その下のselectが一体どうなってしまうのか、非常に不安になった。もちろん、parsedは昇順に並び替えられた順序でやってくる(select句はSelect拡張メソッドに対応している。OrderByの列挙子で順番に値が渡される事を思い出そう)。
では、randomValueはどうなのか?

上のクエリ構文を見ていると、randomValueはorderbyとは関係がないように見える。実際、その下のselect句でrandomValueをそのまま使っている。と言うことは?ソートされるのはparsedの値だけで、randomValueはソートされずに渡されるのか?すると、組み合わせるべき結果がメチャメチャになってしまうのではないか?
(いやいやいや、GetEnumeratorで渡されるというインフラを無視して、ワープしたかのように別のルートで値を渡すなど不可能、不可能なハズだ…?)

もし、let句が無く、varと「書けた」としたら、益々この罠に掛かったことだろう。

翌々書いておく。「ローカル変数」と「範囲変数」は異なるのだ。異なるのだ。異なるのだ。
上記のクエリ構文をメソッド構文に置き換える事が出来るだろうか? この罠に掛かってからは、メソッド構文のイメージが全く掴めなかった。結局、ILSpyでコードを確認して、以下のように変換されることが分かった。

var values = new string[] { "234.56", "456.78", "345.67", "123.45" };
var rand = new Random();
var results =
    values.
    Select(delegate(value)
        {
            var parsed = double.Parse(value);
            return new  // 不可視の匿名クラス
            {
                parsed = parsed,    // let parsedに対応
                randomValue = rand.Next((int)parsed)    // let randomValueに対応
            };
        }).
    OrderBy(delegate(entry)
        {
            return entry.parsed;
        }).
    Select(delegate(entry)
        {
            return new
            {
                R = entry.parsed,
                Length = entry.parsed * 2 * Math.PI,
                Rand = entry.randomValue
            };
        });

letのあるべき部分に、上記のような不可視の匿名クラスが作られ、Selectで一旦射影される。つまり、parsedとrandomValueはその時点の値がセットとして扱われる。だから、直後のOrderByでソートされても、parsedとrandomValueが誤った組み合わせになる事は無い。最後のSelectまで、この組み合わせは維持される。

letとletの間にwhereを入れたりした場合でも、新たな不可視の匿名クラスが作られ、Selectで射影される。分離されたletが存在するだけ、匿名クラスが増えることになる。これはつまり、分離されたletが沢山あるほど、匿名クラスの種類が膨れ上がると言う事だ。まあ、それはまだいい(コンパイラが勝手にやっていることだから)が、その度にnewされるというのは気持ちの良いことではない。この匿名クラスの生成とnewはかなりパターン化していると思われるので、ひょっとするとJITは何か工夫するかもしれない。

実際、範囲変数を正しく振る舞わせる為には、上記のようにするしかないだろう。で、メソッド構文が最も正確にLINQを表現できるとしても、上記のようなコードを書くのは面倒だ。やはりletが使えるクエリ構文が手放せない。私はいつも直感で書くのであまり注意していなかったが、自分でパターンを考えてみると、基本はクエリ構文で書き、letが不要な場合にはメソッド構文を選択しているようだ。

しかし、クエリ構文で書くデメリットもある。以前に述べた拡張メソッドとの相性の問題もあるが、クエリに問題があった場合に追跡が面倒だ。メソッド構文で書いていると、適当な場所でパイプラインをぶった切って、ToList()をかますことで、そこに流れたデータを直接デバッガで可視化出来る(データ量が多いとこの方法は使えないが、デバッグ時だから与えるデータを工夫すればいい)。

クエリ構文だと「ぶった切る」事が出来ない。letを使っているとクエリが複雑化しやすい上にこの制限があるのが辛い。
将来のVisual Studioでパイプラインの流れを可視化出来る何かが追加されると、非常に強力なのだが…
#使った事は無いが、Resharperはクエリ構文をリファクタリングで分割出来るようだ。さすがに強力。

LINQは本当に強力だ (6) TextFieldContext

抽象的な話ばかり続いたので、今回は、実用的な例を示そう。
.NETでCSVファイルを読み取るとき、まさか自分でパースしたりしていないと思うが、知っていると便利なクラスが「VB.NET」のライブラリに存在する。TextFieldParserクラスだ。VB向けの実装の割には、Streamからの読み取りに対応しているなど、割としっかり作ってある。
今回はこのクラスをLINQで「楽に」使えるようにする。

public static class TextField
{
    // 指定されたCSVファイルへのコンテキストを生成する
    public static IEnumerable<string[]> Context(
        string path, string separator = ",", Encoding encoding = null)
    {
        using (Stream stream =
            new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read))
        {
            using (TextFieldParser parser =
                new TextFieldParser(stream, encoding ?? Encoding.UTF8, true, false))
            {
                parser.TextFieldType = FieldType.Delimited;
                parser.Delimiters = new[] { separator };
                parser.HasFieldsEnclosedInQuotes = true;
                parser.TrimWhiteSpace = true;
                while (parser.EndOfData == false)
                {
                    string[] fields = parser.ReadFields();
                    yield return fields;
                }
            }
        }
    }
}

このクラスのアイデアは、LINQ to Entitiesだ。Contextメソッドで一旦「コンテキスト」を作ってしまえば、面倒な事を一切考えなくても読み取りが可能になる。これでロジックから解放だ。
郵便局 郵便番号データ(全国)を使う。

static void Main(string[] args)
{
    // 郵便番号データのコンテキストを生成
    IEnumerable<string[]> context = TextField.Context(
        @"KEN_ALL.CSV", ",", Encoding.GetEncoding(932));

    // (クエリは遅延実行なので、以下の式ではまだ処理を開始していない)
    IEnumerable<string> results =
        // コンテキストからフィールド群を取得
        from fields in
            context.
            AsParallel() // 並列化
        // 郵便番号データの住所部分に「字」と「条」が含まれている行を抽出し、
        where fields[8].Contains("字") || fields[8].Contains("条")
        // 郵便番号の降順でソートし、
        orderby fields[2] descending
        // 文字列形式にフォーマットする
        select string.Format("〒{0} {1}{2}{3}", fields[2], fields[6], fields[7], fields[8]);

    // 取りあえず、結果をコンソールに出してみた(ここでGetEnumeratorが呼ばれて実行が開始される)
    foreach (string result in results)
    {
        Console.WriteLine(result);
    }
}

データ量が少ないので、並列化の恩恵はあまりないが、AsParallelしてみた。AsParallelを付けたり外したりしてみて、タスクマネージャのCPUリソースの具合を確認して見ると良い。

前回、LINQのパイプライン実行について説明したが、上記のコードの良い点は、コンテキストからデータが流れるようにやってきて、クエリで加工されて結果が出力される(コンソールに表示される)というところだ。実は、orderby句(OrderByDescending拡張メソッド)は、内部でバッファリングを行っているので、件数に応じてメモリ使用量が増加してしまうが、orderbyが無ければゴミをGCが回収するため、メモリ使用量が増え続けてしまう事はない。
(orderbyはソートを行うのだが、ソートを行うためにはすべてのデータが揃わなければならない。いくらパイプライン実行でも、不可能なことはある。その代わり、AsParallelしていれば、スレッド数に応じてソートが高速化される。)

さあ、仕上げだ。目の前にCSVファイルの山があったとしよう。

static void Main(string[] args)
{
    IEnumerable<string> results =
        from path in
            Directory.GetFiles("CSV_FILES", "*.csv", SearchOption.AllDirectories).
            AsParallel()
        from fields in
            TextField.Context(path, ",", Encoding.GetEncoding(932))
        where fields[8].Contains("字") || fields[8].Contains("条")
        orderby fields[2] descending
        select string.Format("〒{0} {1}{2}{3}", fields[2], fields[6], fields[7], fields[8]);

    foreach (string result in results)
    {
        Console.WriteLine(result);
    }
}

涙が出るほど簡単で、しかも速い。鳥肌が立つね :-)