2006 年 11 月 29 日 23 時 56 分

スレッディングモデル


このアーカイブは同期化されません。 mixi の日記が更新されても、このアーカイブには反映されません。


インプロセスサーバの DLL が完成しても、
そのままでは COM 基盤はその存在を知る事ができない。
そこで、COM 基盤からサーバを呼び出せるように、
レジストリに情報を登録する必要がある。

COM クラスの情報は、HKEY_CLASSES_ROOT\CLSID の配下の、
{クラス ID} のキーに格納する。
キーのデフォルト値には、クラスの説明文字列を入れる。
これは人間が利用するための情報であり、必須ではない。

インプロセスサーバを登録する場合は、
更に InProcServer32 というサブキーを作成し
キーのデフォルト値に、DLL のファイル名を入れる。
これで、COM クラスと提供サーバの関連付けが行われるのだ。

さて、インプロセスサーバは上記の登録だけで利用可能だ。
早速登録と行きたいところだが、
ここで、スレッドの話をしておかなければならない。

COM サーバには、スレッディングモデルという規約がある。
スレッディングモデルとは、COM クラスが、
どのようなスレッド環境で動作するかどうかを示している。

上記の設定の場合、COM サーバへのあらゆる呼び出しは、
単一のスレッド上で行われることになる。
このモデルを Single-threading Model と呼ぶ。

たとえ、クライアントがマルチスレッドであり、
クライアントの複数のスレッドで呼び出しを行ったとしても、
COM 基盤が間に入って代行処理を行い、
サーバ側に対しては常に単一のスレッドから
呼び出されることが保障されるのだ。

今までのコードで、スレッドのことを考えていなかったのは、
COM 基盤がこのような機能を持っているからなのである。

今回作成しているのはシェルエクステンションなので、
シェルに対する特別な制約が存在する。

上記のような、Single-threading Model のサーバは、
完全なシングルスレッドで動作する。
マルチスレッドで動作するシェルから利用する事を考えると、
COM 基盤のオーバヘッドは無視できず、効率が非常に悪い。

そのため、シェルエクステンションとして動作するサーバは、
マルチスレッドに対応したモデルである、
Single-threaded Apartment Model への対応が要求される。
これは略して STA Model とも呼ばれる。

一般的に、完全なマルチスレッドに対応するには、
そこそこ骨の折れるコーディング作業を要するため、
COM では、マルチスレッドにも段階を設け、
幾つかの動作モデルを用意している。

その中で、最もコーディングが楽なのが STA Model である。

STA Model もマルチスレッドの一種なので、DLL の関数が
複数のスレッドから同時に呼び出される可能性がある。
そのため、DllCanUnloadNow や、DllGetClassObject、
がスレッドセーフであることが必須となる。

ただ、STA Model は、COM クラスのインスタンスに関しては、
あるインスタンスに対するメソッド呼び出しが、
必ず同一のスレッドから呼び出されることを保障する。

つまり、一度作成されたクラスのインスタンスは、
作成の際に使われたスレッドに関連付けられ、
そのインスタンスへのメソッド呼び出しは、
どのインタフェースを使っているかに関わらず、
常にそのスレッド上で呼び出されるということになる。

そのため、COM クラスを実装する際の手間が少し軽減される。
クラスのメンバやメソッドの引数にしかアクセスしないなら、
一切スレッド対応処理を書かなくても良い事になるのだ。

ということで、折角できたと思った DLL を
STA に対応させるために書き換える必要がある。

幸い、現在のソースでスレッドセーフになっていない部分は、
g_serverLockCount に関する部分だけである。

現実装はクラスオブジェクトをシングルトンとしており、
複数のスレッドから呼ばれる DllGetClassObject が返す。
つまり、クラスオブジェクトは複数のスレッド使われるので、
クラス全体をスレッドセーフにしなければならないのだが、
満たしていないのは g_serverLockCount だけだ。

次に、HardLinkIconID クラスに関しては、
クラスオブジェクトの CreateInstance で、
毎回 new しているため、インスタンスの使いまわしはない。
そのため、常に new されたスレッドで呼び出されるので、
クラスに手入れを行う必要はない。

ただ、継承している Object クラスが、
やはり g_serverLockCount にアクセスしているので、
この部分はスレッドに対応しなければならない。

Windows API には、InterlockedIncrement と、
InterlockedDecrement と言う打ってつけの関数がある。
これは、マルチスレッド環境において、
自動的に変数への排他アクセスを行い、
確実にインクリメントとデクリメントを行ってくれるのだ。
これを利用すると修正は非常に簡単になる。

グローバル変数宣言を以下のように書き換える。(global.hpp)
変数の型も、合わせて long (又は LONG) にしておこう。
どうせカウンタの値は 0 かそうでないかしか見てないし。

旧: extern ULONG g_serverLockCount;
新: extern volatile long g_serverLockCount;

変数定義も以下のように書き換える。(global.cpp)

旧: ULONG g_serverLockCount = 0;
新: volatile long g_serverLockCount = 0;

カウンタの増減が使われている以下の部分も書き換える。

・Object::AddRef
・Object::Release
・HardLinkIconIDClass::AddRef
・HardLinkIconIDClass::Release
・HardLinkIconIDClass::LockServer

旧: ++g_serverLockCount;
新: InterlockedIncrement(&g_serverLockCount);

旧: --g_serverLockCount;
新: InterlockedDecrement(&g_serverLockCount);

これだけの変更で、STA Model として利用できるようになる。

前にも述べたが、Object::AddRef と Object::Release での、
g_serverLockCount の増減は、
サーバをロックするためのものなので、
毎回増減する必要はない。

COM クラスのインスタンスの参照カウンタは、
インスタンスごとに独立しているので、
STA Model では単なるインクリメント・デクリメントで良い。

そのため、g_serverLockCount の増減を、
インスタンスの参照カウンタが、
0 から 1 になる際にのみ増やし、
1 から 0 になる際にのみ減らすようにすることで、
最大のパフォーマンスが得られるようになる。



Copyright (c) 1994-2007 Project Loafer. All rights reserved.