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は「インターフェイス境界」が非常にはっきりしている。そのため、この境界を基準として、ウインドウに紐付いた暗黙のスレッドとの競合についても、背後で面倒を見ようとしたわけだ。

つづく

投稿者:

kekyo

A strawberry red slime mold. Likes metaprogramming. MA. Bicycle rider. http://amzn.to/1SeuUwD