(追記: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のインスタンスのメソッドを呼び出したりする)、この擬似的な「部屋」によって挙動が変わることになる。
例えば、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で操作する機会が(一般的には)あまり無いからだ。必要なければ、アパートメントを設定する必要もない。