ChalkTalk CLR – COMのすべて

WP_20151219_11_31_11_Pro_LI本日は「ChalkTalk CLR – COMのすべて」と題して、COM(Component Object Model)についてのディスカッションを行ってきました。

参加された方はCOM方面に強い方が半数近くいて(MVP4人、元MVP2人、中には現役で開発やってるという人も)、とにかくこれ以上ないぐらい強力なメンバーで、濃い議論が交わされました。多分、もうこういう企画は無いかな感全開 (;´Д`) 遠方からの参加もありがとうございます。 ChalkTalkバンジャーイ

この記事では、内容を「要約」して記載します。図については内容を考えて起こしなおしました。

前回と同じく、Maker Lab NAGOYAさんのスペースをお借りしました。ありがとうございました。


事前の洗い出し

WP_20151219_18_18_54_Proまず、各参加者のCOMに対してのスキルセットを「バリバリ解説OK」~「COMって何」のレベルで、かつ自分がCOMに対して抱えている課題、という切り口で、いつも通り付箋に5分で書いてもらって張り出しました。

写真は横に集約した後の状態なので分かりにくいですが、上から「COMって何」~「バリバリ解説OK」の順で並べてあります。要約すると:

  • COMの概念的なものへの疑問
  • COMとWinRTとの関係
  • COMの将来展望
  • COMの実践的な実装方法
  • スレッドアパートメントとは何か
  • マーシャリングとは何か

という内容が全体的に知りたいとことと認識できたので、順に掘り下げていきました。


COMの概念的なものへの疑問

そもそもCOMとは何で、何のための技術?技法?なのか? という基本的な疑問です。ディスカッションでは、Essential COM第一章や、Inside OLEでのIUnknownインターフェイスの実装例の話から:

  • 処理系の問題1: リンカー(C言語の)のリンケージの問題
  • 処理系の問題2: メソッドの呼び出し規約の問題
  • コンポーネントインスタンスの生存期間の問題
  • インターフェイス型の認識の問題

と言う問題の解決に集約されるという意見交換がなされました。

処理系の問題

処理系の問題とは、C/C++コンパイラ・リンカーが、それぞれの製品(例:VC++、gcc、BC++など)で、シンボル名の内部的な命名規約が独自仕様である事と、メソッドのエントリポイントが特定されたとしても、その呼び出し規約が異なる事がある、と言う事です。

処理系によってシンボル名が異なる(特にC++において)問題は、シンボル名に引数の型情報を含ませることから発生します。このシンボル名の操作の事を「マングリング(Mangling)」と呼び、その仕様は処理系によって異なります。また、同じ処理系でもバージョンによっても異なる可能性があります。

dependencywalker1(.NETではなく)Win32レベルでのDLLのエクスポートシンボルを調べるツールとして「Dependency Walker」と言うツールがあります。これを使ってEXEやDLLを調べると、エクスポートシンボルの名前が分かります。

このスクリーンショットでは「KERNEL32.DLL」のエクスポートシンボルを見ています。赤枠に表示されていますが、普通にWin32 APIのシンボル名が見えます。シンボル名が普通に見える関数は、呼び出し規約が「stdcall」である事を示しています。

dependencywalker2対比する形で、「MSVCRT.DLL」を見てみます。赤枠を見ると、「exception」とか「badcast」とか読み取れる単語もありますが、何やらわけのわからない記号が沢山見えます。これがマングリングされた状態のシンボル名で、VC++のシンボル名規約に従って変換された結果です。

dependencywalker3そういえば、実際のところこれがC++のどんなシンボルに相当するのかを実演しなかったのを思い出しました。これが、デマングリングした結果です(メニューから「Undecorate C++ symbol」を選択すると表示されます。見比べてみると面白いと思います)。

このような、C言語で言うところの関数名だけでは表現できない、C++のオーバーロードやクラスメンバ関数、const関数などを、シンボル名だけで識別可能にするために、マングリング操作が行われます。そしてこのマングリング操作が、処理系依存(かつバージョン依存)なのです。

したがって、例えばVC++ 6.0で普通に作ったDLLとVBで作ったDLLを直接静的にリンクすることは出来ません。他の処理系についても同様です。COMは、これらの処理系依存シンボルを使わず、COMのランタイムが提供する情報だけでクラスのファクトリを特定し、メンバ関数の位置はvtableと呼ばれる関数ポインタのテーブルの並びの整合性を担保することで、この問題を回避します。

# vtableの話はディスカッションでやらなかったですね。vtableはC++のvtableそのままで、仮想関数へのポインタのテーブルです。

どうやってクラスを特定するのかは、レジストリのHKEY_CLASSES_ROOT配下の情報を使います。ATLでプロジェクトを新規に生成すると、プロジェクト内に*.rgsのようなファイルが配置されます。これはコンポーネントレジストラスクリプトと呼び、要するにregsvr32された時にレジストリのどこにどんな値を書き込むのかを表し、このスクリプトでCOMのクラス(coclass)が特定できるようにします。

# 特定には、COMコンポーネントの実装が、どこのパスにあるDLL内にあり、どのクラス(CLSID –> GUID)なのかという情報を含みます。

もう一つの問題がメソッドの呼び出し規約で、これはWikipediaにあるように複数の規約があり、VC++では各関数単位でこれを指定して実装できるのですが、COMでは例外なく「stdcall」を使います。

生存期間

生存期間とは、COMのクラスのインスタンスが、いつまで生き続けるのかと言う問題です。.NETのインスタンスは、誰からも参照されなくなり、GCが回収する(場合によってはファイナライザーが呼び出され)と死んだ事になるのですが、C言語のようなアンマネージな世界では、「malloc」や「new」によってインスタンスが(ヒープに)生成され、「free」や「delete」によってメモリから取り除かれます。

// 確保する
auto p = static_cast<int*>(malloc(123 * sizeof(int)));

// 使う...

// 解放する
free(p);

このような簡単な例であれば、確保して解放するというサイクルから、解放のタイミングの時点で正しく解放されることが分かります。しかし:

static const int* pUseLate = nullptr;

static void ComputeStep1(const int* pTarget)
{
	// 後で使いたいので保存
	pUseLate = pTarget;
}

static void ComputeStep2()
{
	// (pUseLateを使って何かやる)
}

// 確保する
auto p = static_cast<int*>(malloc(123 * sizeof(int)));

// 使う...

// 処理その1
ComputeStep1(p);

// 処理が終わったので解放する
free(p);

// 処理その2
ComputeStep2();

このような処理があった場合、ComputeStep2の内部実装では、既にpが解放されていることを知る由はありません。このサンプルコードは非常に短く、メインの処理とComputeStepの実装が並べて書いてあるので「こんなのダメに決まってるじゃん!」と分かりますが、ComputeStepの実装は「別のだれか」がやるとしたらどうでしょうか? あるいは、既にComputeStepは別の誰かが実装済みであり、これからメインの処理を書く場合、ComputeStep2の実行前にpを解放してはならない事はどうやって分かるのでしょうか?

このような、インスタンスの生存期間の問題を解決する方法として、COMではクラス実装側に参照カウンタを持ち、このカウンタ値を監視することで不要になったかどうかを判定します。

生存期間のカウンタは、IUnknownインターフェイスAddRefによってカウントアップされ、Releaseによってカウントダウンされます。カウンタの初期値は1で、AddRefのたびにインクリメントされ、Releaseが呼び出されるとデクリメントされます。カウンタが0になると、自己消滅するように実装します。

使用する側は、ポインタをコピーするときにはAddRef、使い終わったらReleaseするという規約を守る事により、生存期間が正しく管理されるようになります。

インターフェイス型の認識

インターフェイス型の認識の問題もあります。C++上でインターフェイスを示すポインタをキャストして、目的のインターフェイスへのポインタを取得しようとしたとします。

// CoCreateInstanceなどで取得したインターフェイスポインタ
IUnknown* pUnknown = ...;

// 目的のインターフェイスポインタを得るためにキャスト
ICalc* pCalc = static_cast<ICalc*>(pUnknown);

このキャストが正しく解釈されるかどうかは、処理系に依存します。IUnknownをICalcにするにはダウンキャストが必要ですが、C++の世界ではコンパイラがその判断を行えるものの、COMの世界ではインターフェイスの情報しか存在しないため、正しくキャストできる保証がありません。そのため、キャストと言う処理自体も、IUnknown.QueryInterfaceメソッド呼び出しで解決します。

// CoCreateInstanceなどで取得したインターフェイスポインタ
IUnknown* pUnknown = ...;

// 目的のインターフェイスポインタを得るためにキャスト
ICalc* pCalc = nullptr;
pUnknown->QueryInterface(IID_ICalc, reinterpret_cast<void**>(&pCalc));

第一引数はインターフェイスID(GUID)で、これが必要なのはCOMの世界は.NETとは異なりリフレクションが存在しないので、インターフェイス型を一意に識別するIDが必要だからです。勿論、この場合のIID_ICalcは、ICalcインターフェイスのIDである前提です。

QueryInterfaceメソッド内でキャストを実装することにより、処理系の範囲内で正しくキャストが行えます。返されるポインタはキャスト後のポインタであるため、呼び出し元は処理系に依存することなくキャスト出来たことになります。

このようなメソッドを提供することで、処理系に(C++固有の)型を認識させる必要をなくし、互換性を維持します。

COMの概念への展開

結局、IUnknownの実装を通じて、どうやって処理系に依存しないでバイナリ互換性を維持するのかという事が、COMの中心にある概念であることを確認しました。COMのIUnknownとその実装はこんな感じだ!というのを、C#ライブコーディングで疑似実装して、参照カウンタの動きや、QueryInterfaceによるキャストの挙動を示して、内容の共有をしました。

# 以下のコードは、クラスファクトリも含めて穴埋めしたコードです。

// (あくまで概念コードです。これは正式なCOMのコードではありません)
public interface IUnknown
{
	T QueryInterface<T>() where T : IUnknown;
	int AddRef();
	int Release();
}

public interface ICalc : IUnknown
{
	int Add(int a, int b);
}

// クラスは直接公開されない
internal sealed class CalcImpl : IUnknown, ICalc
{
	private int count_ = 1;

	public int AddRef()
	{
		return Interlocked.Increment(ref count_);
	}

	public int Release()
	{
		var current = Interlocked.Decrement(ref count_);
		if (count == 0)
		{
			// インスタンスの破棄処理
			// Disposeに近いが、メモリストレージも削除される。
			// 破棄処理を隠蔽することで、処理系依存を排除。
		}
	}

	public T QueryInterface<T>() where T : IUnknown
	{
		// CやC++にはリフレクションは無いので、本当はインターフェイスID(GUID)で判定する。
		// キャスト処理を隠蔽することで、処理系依存を排除。
		if (typeof(T) == typeof(ICalc))
		{
			Interlocked.Increment(ref count_);
			return (T)(object)this;
		}
		if (typeof(T) == typeof(IUnknown))
		{
			Interlocked.Increment(ref count_);
			return (T)(object)this;
		}

		throw new NotImplementedException();
	}

	public int Add(int a, int b)
	{
		return a + b;
	}
}

public interface IClassFactory
{
	T CreateInstance<T>(Guid clsid) where T : IUnknown;
}

// クラスファクトリクラスも公開されない
internal sealed class CalcClassFactory : IClassFactory
{
	public T CreateInstance<T>(Guid clsid) where T : IUnknown
	{
		if (clsid == CLSID_Calc)
		{
			// newの実装を隠蔽することで、処理系依存を排除。
			return new CalcImpl();
		}

		throw new NotImplementedException();
	}
}

// CalcImplを外部から特定するためのGuid
public static readonly Guid CLSID_Calc = new Guid("BD4D1DDD-9C28-4432-A8DD-9CFA77E6433F");

// DLL外からはこのエントリポイントだけが見える
public static IClassFactory DllGetClassObject()
{
	return new CalcClassFactory();
}

もう既にかなりアレですが、まだまだ続きます。


COMとWinRT

discussion_allaboutcomWinRT(ストアアプリ・UWPのフレームワークとして使われるライブラリ)は、基礎がCOMで出来ていることは分かっていましたが、それ以上の理解は私の中になく、あんまり調べる意欲もなかったので、知りたかった部分です。

全てのCOMクラスはIUnknownインターフェイスを実装しますが、WinRTのクラスは、IInspectableインターフェイスも実装します。COMの世界ではITypeInfoインターフェイスをタイプライブラリ情報を公開するのに使いますが、これのWinRT版のような位置づけです。WinRTでは型情報は.NETのメタデータをそのまま使用します(winmd)。この情報との対応付けにIInspectableインターフェイスを使います。したがって、ITypeInfoのように、情報の細部までトラバース可能なメソッドセットは持っていません。

なお、COMにおいてITypeInfoインターフェイスの実装は任意ですが、WinRTのIInspectableの実装は必須です。

WinRTにおいてのもう一つの事情は、スレッドアパートメントの扱いです。WinRTでは、メインスレッドに相当するスレッドがASTAに属し、それ以外のスレッドはMTAに属するそうです。各アパートメント間の関係は、通常のCOMと同一とのこと。なるほど。

# 余談ですが、MSDNライブラリのリンク壊れてたりするよねー、直してほしいよねーみたいな話が… TechNetもぶっ壊れてるなー |д゚) チラッ

こんな話をすると、当然「アパートメントとは何か」みたいなところを避けては通れないけど、とりあえずは次の課題へ。


COMの実践的な実装方法

ようするに、どうやってCOMを書けば良いのかわからない(.NETとかVB6以外で)。

ATLでやる

ということで、一からIUnknownをCで実装するというハードは方法は時間もないのでパスし、ATL(Active Template Library)での実装方法をライブデモしました。ライブデモは私がやったのですが、ATLでバリバリ書いていたのはVisual Studio 2005の時だったので、2005を使ってデモ(こんなこともあろうかと、2005はインスコしてあるのだよ)。ATLは途中から、C#のような属性ベースの指定で楽に書けるようになったはずなのですが、結局私的には移行しなかったので分からず…

atlwizard新しいプロジェクトの追加で、「ATLプロジェクト」を追加し、「プロジェクト-追加-クラス」で、「ATLシンプルオブジェクト」を選択すると、COMをATLで実装するクラスのひな型が追加されるウィザードが表示されます。短い名前の所にクラス名を入れると、各ファイル名とか良しなに決定されます。問題は次のウィザード。

ここのスレッドモデルとアグリゲーションの選択も、結構ディープだよね、ああそれとフリースレッドマーシャラーとかも。でもこの説明をするには、やっぱりアパートメントを避けては通れないしデモが中断してしまうので、ちょっと横に置いておく事に。

これでひな型が生成されたら、あとは「クラスビュー」のCOMインターフェイスのツリーから「追加-メソッド」とか「追加-プロパティ」とかやると、メンバを追加するウィザードが表示されて、メンバ定義を簡単に追加できます。もし、手動で定義するとなると、IDLファイル・ヘッダファイル・ソースファイルを同時に修正しなければならないため、かなり面倒です。そして、追加はウィザードで簡単にできますが、修正するとなると同じように全部直さなければならないので、結構苦痛…

で、実装出来たらビルドしますが、Visual Studioが管理者権限で起動していないと、最後にエラーが出ます。これはregsvr32でDLLを登録しようとしたときに、コンポーネントレジストラスクリプトがレジストリを変更するためで、素直に再起動。

TestPropという文字列(BSTR)のプロパティを追加し:

// メンバには以下のようなフィールドを定義
private: _bstr_t temp;

STDMETHODIMP CTest::get_TestProp(BSTR* pVal)
{
	// TODO: ここに実装コードを追加してください。

	*pVal = temp.copy();
	return S_OK;
}

STDMETHODIMP CTest::put_TestProp(BSTR newVal)
{
	// TODO: ここに実装コードを追加してください。

	temp = newVal;
	return S_OK;
}

テストするコードはC#で書きました。
COM側のビルドとregsvr32が完了していれば、C#の参照設定でCOMコンポーネントが参照出来ます。

using ATLSampleLib;

class Program
{
	static void Main(string[] args)
	{
		var testClass = new TestClass();
		testClass.TestProp = "ABCX";
		Console.WriteLine(testClass.TestProp);
	}
}

これで無事、コンソールに”ABCX”が表示されました。

WinRTでは文字列をHSTRING型で扱いますが、これはリテラル文字列をいちいちメモリ確保してコピーして…というコストを削減するためだそうです。なるほど、それだとCLRの世界での文字列と同じように扱われる訳で、イメージは湧きやすい。BSTRを使うと、内部ではSysAllocString APIなどでメモリを確保したり解放したりするので、コストは高いです。

いつ解放されるのか

上記テストコードで、AddRefもReleaseも呼び出していないのは?という疑問に対し、CLRの世界にはRCW(ランタイム呼び出し可能ラッパー)があって、これがAddRefとかReleaseを呼び出しているのだ、しかし、この例では使用後にすぐプロセスが終了してしまうので、果たして呼び出されるのか?という疑問があり、じゃあ、Marshal.FinalReleaseComObjectを呼び出せばどうだ、いや、それを呼び出すと挙動がおかしい、などの説があって、試しました。

using ATLSampleLib;

class Program
{
	static void Main(string[] args)
	{
		var testClass = new TestClass();
		testClass.TestProp = "ABCX";
		Console.WriteLine(testClass.TestProp);

		Marshal.FinalReleaseComObject(testClass);
	}
}

ATL側では、インスタンスの寿命が尽きる時(参照カウンタが0になったとき)、FinalReleaseメソッドが呼び出されます。ここにブレークを張ってテストしたのですが、やってこない…

FinalReleaseComObjectの代わりにGCを手動実行したらどうだろうと言う事でテスト:

using ATLSampleLib;

class Program
{
	static void Main(string[] args)
	{
		var testClass = new TestClass();
		testClass.TestProp = "ABCX";
		Console.WriteLine(testClass.TestProp);

		GC.Collect(2, GCCollectionMode.Forced, true);
		GC.Collect(2, GCCollectionMode.Forced, true);
		GC.Collect(2, GCCollectionMode.Forced, true);
	}
}

すると来ましたFinalRelease。うーむ、FinalReleaseComObjectとは何なのか? これだとインターフェイスポインタを直接弄った方が安心感があるよなぁと思わなくもない(真相は分からず)。

finalreleasestacktrace1もう一つ気が付いたことが。FinalReleaseでブレークを張ったところ、スタックトレースがこのように。

これワーカースレッドから呼ばれてるよね、何でだ?と思ったらMainメソッドに、STAThread属性を付けていなかった。

finalreleasestacktrace2で、STAThreadを追加したところ、無事にメインスレッドから呼び出された(このCOMコンポーネントがワーカースレッドから呼び出されること自体に問題はないんですが、一瞬えっと思ったので気が付いた次第)。

# しかし、このスタックトレースも大概だなぁ。当然こうなる事は予測できるんだけど…

さて、これの何が問題かと言う事は、やっぱりアパートメントの話に踏み込まないと説明できないよね。と言う事で、そろそろ参加者の意識共有も出来たので、アパートメントを話をし始めました。


アパートメントとは何なのか?

apartment_diagramアパートメントとは、スレッドとCOMのインスタンスをグループ分けする概念で、そのグループとは何なの?というディスカッションをしました。グループの種類は「STA(シングルスレッドアパートメント)」と「MTA(マルチスレッドアパートメント)」の二種類(あと、概念的にはメインSTA)があります。

アパートメントの概念が難しいのは、用語の説明から入らざるを得ないからかなーと思います。このような図が一般的に説明に用いられますが、内部実装的にこうなっているわけではなく、あくまで概念的に分割される、という事も、難解なところかなと。混乱すると良くないので、内部もこの通りになっていると仮定してもいいんじゃないかと思います(実際は違うって事が分かるようになったら、もう説明も不要になってるはず)。

シングルスレッドアパートメント

一つのスレッドだけが特定のSTAに属します。新たなスレッドを作り、STAに属させると、図の通り新しいSTAが作られます。メインスレッドがSTAとなった場合は、特別に「メインSTA」と呼びます(実際には、プロセス内で最初にSTAとして初期化されたスレッドです)。スレッドをSTAに属させるには、CoInitializeEx APIを実行します。.NETの場合は、メインスレッドであれば、Mainメソッドに「STAThread属性」を適用し、ワーカースレッドを作る場合は、Thread.SetApartmentStateメソッドで指定します。

図のように、STAは他のアパートメントと(概念的に)分離されており、そこに属するスレッドやインスタンスは、別のアパートメントから直接参照することが出来ないという原則に従います。

マルチスレッドアパートメント

MTAは、プロセス内にただ一つだけ存在します。そして、MTAに属するスレッドは複数存在できます。スレッドをMTAに属させる方法も、STAとほぼ同等です。Mainメソッドに適用する場合は、「MTAThread属性」を使用します。

STAとアパートメント間の特性

apartment_diagram2STAとMTAの最大の違いは、STAは「ウインドウメッセージキュー」に依存していると言う事です。今までのCOMの説明にはウインドウとかコントロールのようなユーザーインターフェイスの話は全く出て来なかったので、「えっ」てなってました。

ここで説明の順序を逆にして、遠い昔、Win16(つまりWindows 3.1以下)の頃は、マルチスレッド対応がそもそも無く(_beginthreadex APIとか存在しない)、すべての操作を非同期的に処理するためには、メッセージポンプにメッセージを配信する形で協調的に動作させる(ノンプリエンティブマルチタスク)必要がありました。

この頃に作られたアプリケーションは、マルチスレッドのように横から突然割り込まれてメソッドが実行されるような事を全く想定していません(スレッドがそもそも存在しないので)。今でいうところのモニターロックや排他制御がまったく不要だったのです。そのため、マルチスレッド環境で別のスレッドからこのような実装を呼び出すと、容易に破たんします。そして、現在のユーザーインターフェイスも全く同様の構造なのです。

つまり、異なるスレッドからウインドウ要素を操作したりする場合は、すべからく安全策をとる必要があります。WPFであればDispatcherを使う、Windows FormsであればInvokeメソッドを使って簡単に呼び出しの「マーシャリング」を実行できます。しかし、COMが考案されたころには、そのような強力な道具立てがなかったのです。

更に、既存のマルチスレッド非対応コードがバイナリライブラリとして「変更不能」な状態で存在するため、互換性も担保する必要があります。この問題をフレームワーク(COMランタイムライブラリ)で隠蔽し、どうにかして安全性と実装の軽減を両立させたかったのだと推測されます。

apartment_diagram5STAがウインドウメッセージキューに依存しているとは、STA内のCOMコンポーネントインスタンスを操作する場合は、このキューに一旦要求(メソッド呼び出し情報)を格納し、「協調的」に処理させると言う事です。

勿論、STA内のスレッドであれば、同じSTA内のインスタンスは、キューを介することなく操作できます。勿論、メソッド呼び出し結果の返却も、キューに一旦結果を格納して受け取ります。このときのキューは、呼び出し側のキューを使います。

このように、ウインドウメッセージキューを介在させることで、ウインドウメッセージ(ウインドウの移動やリサイズ・ボタンのクリック・テキストの変更など)にいきなり割り込むことなく、順番に規則的に実行されるので、マルチスレッド競合について考えなくても良くなる、というのが、STAに属する利点となり、同時に高いオーバーヘッドが欠点となります。

apartment_diagram6ところで、STAとMTA間で呼び出しを処理する場合は、呼び出し側がMTAであれば、呼び出し時はSTAのキューを使いますが、処理結果はキューに入らず、直接元のスレッドに伝達されます。逆であれば、呼び出しがキューに入らず、結果は一旦キューに入ります。

これは、MTAは特定のウインドウメッセージキューに紐づく事がないからです。したがって、ユーザーインターフェイスを含むCOMコンポーネントをMTAに属させると、クラッシュするなどの予期しない結果を引き起こします。

そして、MTAには複数のスレッドが同居出来て、MTA内のCOMインスタンスはすべて介在される事なく直接呼び出しが成立するので、ほぼコスト0で高速にメソッド呼び出しが行われます。これが、IISにホストされるCOMコンポーネントをSTAではなくMTAで動作させなければならない理由です(MTAにするとパフォーマンスが向上するらしい、という理由で、背景を考えずにMTAに属させるコードを書くと、間違いなくハマります)。

COMインスタンスはどこのアパートメントに配置されるか

atlwizardあるアパートメントに属しているスレッドがCOMコンポーネントのインスタンスを生成(CoCreateInstance API)すると、コンポーネントに指定された属性に従って、インスタンスが配置されるアパートメントが変わります。再びATL COMオブジェクトウィザードのスクリーンショットですが、ここで、「スレッドモデル」を選択可能です。

このスレッドモデルの選択と、現在のスレッドがどのアパートメントに属しているのかによって、配置されるアパートメントが決定されます。昔、この組み合わせの表をどこかで見た記憶があるのですが、改めて書き出してみました(ただし、すべて検証しているわけではないので、間違っているかもしれません)。

メインSTA STA MTA
シングル メインSTA メインSTA メインSTA
アパートメント メインSTA STA STA
両方(Both) メインSTA STA MTA
フリー(Free) MTA MTA MTA
ニュートラル メインSTA STA MTA

この表を眺めていると、何を選択すべきかが見えてきます。

  • ユーザーインターフェイス(ウインドウメッセージを必要とする)を含むコンポーネントであれば、「アパートメント」を選択する。これには、俗に言う「ActiveXコントロール」も含まれます。
  • その中でも特にメインスレッド(メインSTA)でのみ動作可能なコンポーネントであれば、「シングル」を選択する。
    (今やこのようなコードは想像しにくいかもしれません。例えばグローバル変数に状態を持っていて、これを操作しているようなUIコンポーネントは、多分メインSTAでしか動作しません)
  • アパートメントの影響を受けないコンポーネントであれば、「両方」又は「ニュートラル」を選択する。
  • MTAにのみ必ず属させ、ハイパフォーマンスでローコストな環境を必要とするコンポーネントであれば、「フリー」を選択する。

再度確認したのが、MTAに属させたからと言ってハイパフォーマンスになるわけではなく、マーシャリングコストを最小化可能であることがミソなので、ここで不用意に「フリー」を選択すると、アパートメントの選択同様ドツボにはまる事に注意が必要です。

一番無難な選択肢が「両方(Both)」ですが、表の上では「ニュートラル」も同じとなっています。両方とニュートラルの違いは、両方とすると、メインSTA・STA・MTAのどこにでも配置出来るものの、配置されるとそのアパートメントに完全に紐づきます。対して、ニュートラルとは、本当のところは「どのアパートメントにも属さない」状態となります。この違いは、マーシャリングの動作に影響します。


マーシャリングとは

アパートメントの話は抽象的なので、もう少し実装に近寄ります。

apartment_diagram7アパートメントの境界を超えるために、COMのランタイムによって「プロキシ・スタブ」という一種のファサードが自動的に介在し、メソッド呼び出しを自動的にメッセージキューに保存し、目的のSTAのスレッドでこれを取り出して実行する、という処理を行う事を説明しました。一般的にこのような操作を行う事を「マーシャリング」と言います。

マーシャリングは、カスタム「プロキシ・スタブ」コードで完全に独自の実装を行うか、あるいは「COMスタンダードマーシャラー」を使うかを選択できます。正直カスタムマーシャラーは深くて闇で資料も少なく、これを自前で実装する事は殆ど無いと思います。

どちらにしてもこの図のように、アパートメント境界の間に入って、メッセージキューの操作をしたり、スタックを操ってリモートメソッド呼び出しを成立させる役割を担います。呼び出し元は、カスタムマーシャラーが「何故か」目的のインターフェイスを実装したインスタンスに見えるので、アパートメント内呼び出しと全く同じように、インターフェイスメソッドを呼び出す事が出来ます。

# IUnknown.QueryInterfaceメソッドは、インターフェイスIDを受け取って、そのインターフェイスへのポインタを返します。と言う事は、COMスタンダードマーシャラーが、実際には実装を用意していなかったとしても、IID_ICalcのようなインターフェイスIDが存在して、キャストも成立させたように見せかければ、この状況を作り出す事ができます。返されるインターフェイスポインタは、ICalcを動的に実装したスタブクラスです。

さて、このマーシャラーが一体いつ挿入されるのかが、背景を知っていないと分からないと思うため、その説明をしました。

インスタンス生成時

apartment_diagram8CoCreateInstance APIでインスタンスを生成するとき、呼び出し元のスレッドが属するアパートメントと、COMコンポーネントに指定された「スレッドモデル」に応じて、インスタンスが配置されるアパートメントが決定されます。では、呼び出し元スレッドのアパートメントと異なるアパートメントにインスタンスが生成され、配置された場合、呼び出し元はどうやってそのインスタンスにアクセス出来るのか?

答えは、CoCreateInstanceが返すインターフェイスポインタが、「既にCOMスタンダードマーシャラーなどのファサードを示している」ので、呼び出し元は何も考えなくても良い、と言う事です。

この図のように、MTAのワーカースレッドは、CoCreateInstanceが返すインターフェイスポインタを、本物の「ICalc」を実装したインスタンスだと思い込んでいます。しかしその実態はマーシャラーであり、ICalcのメソッドを呼び出すとマーシャラーのメソッドを呼び出し、後は前述のとおり、マーシャリング動作が行われます。

インターフェイスポインタの自動伝搬時

内部的にはここが難しい所で、一度マーシャラー経由のインターフェイスポインタを取得した後は、メソッド呼び出し引数や戻り値にインターフェイスポインタが含まれていれば、マーシャラーが自動的にそのインターフェイスポインタにもマーシャラーを挿入します。

// 呼び出し元の例:
IHogeService* pHogeService = ...;

// 例えば、戻り値がインターフェイスポインタの場合
IResultData* pResultData = nullptr;
pHogeService->GetResult(&pResultData);

// (pResultDataは何を指しているか?)

apartment_diagram9GetResultメソッドの戻り値がIResultDataという、別のCOMコンポーネントインスタンスへのポインタである場合、その実態は呼び出し先のアパートメントに存在する可能性が高いです(必ずと言うわけではない)。

その場合、戻り値として返されるインターフェイスポインタは、あくまで呼び出し先のアパートメントのインスタンスを示しているので、それがそのまま呼び出し元に返されても、その後が困ります(そのまま使うとマーシャリングが行われないので、アパート境界が守られず、酷い問題が発生する)。

そのため、スタンダードマーシャラーは、戻り値のインターフェイスポインタを「マーシャリング」し、新たなマーシャラーを割り当て、マーシャラーのインスタンスへのポインタを呼び出し元に返却します。

呼び出し元がIResultDataのメソッドを呼び出すと、裏ではマーシャラーを介してインスタンスを操作することになり、安全性が担保されます。呼び出し元はIResultDataがマーシャラーである事は知る由もなく、直接呼び出しているのと変わりません。

さて、これを実現するには、引数や戻り値に「インターフェイスポインタが含まれているかどうか」を認識出来なければなりません。CやC++のソースコード上では、インターフェイス型を指定するので(人間の目には)判別できます。しかし、コンパイル後のバイナリには、型情報が含まれないため、自動的に検出する事が出来ません。COMスタンダードマーシャラーが、ノーメンテで動作するには、型の自動判別が可能である必要があるのです。

方法は2つあります。一つは、インターフェイス型のメタデータをタイプライブラリ(*.tlb)として読み取り可能になるようにファイルを配置するか、リソースとしてDLLに埋め込み、「プロキシ/スタブデータ」を提供することです。これにより、COMスタンダードマーシャラーは、引数や戻り値の型情報にアクセスし、それがマーシャリングの対象である事を認識できます。

もう一つは、同じくタイプライブラリを提供する必要がありますが、引数や戻り値の型にVARIANT型を使う事です(プロキシ/スタブデータは不要)。VARIANT型は、格納できるサブタイプを指定して値を格納します。サブタイプの種類にハードコーディングされた制限がありますが、基本的なプリミティブ型やインターフェイスポインタなどを含むことが出来、何が含まれているのかを検知出来ます。但し、VARIANT型を使うと、タイプセーフ性は低下します。

# もちろん、カスタムマーシャラーを書いた場合は、自分でマーシャリングを行うため、タイプライブラリの準備やVARIANT型への依存は不要です。また、IDispatchインターフェイス経由でのみ公開されるメソッドの場合は、すべてVARIANT型である事を想定可能なので、タイプライブラリは不要です。

インターフェイスポインタの手動伝搬時

自動マーシャラーを全く介さないというソリューションもあります。一つは「フリースレッドマーシャラー(FTM)」を集約する事です。詳細は省きますが、この場合はマーシャリングを自分で面倒見なければなりません。もう一つが、スレッドモデルを「ニュートラル」とした場合です。この場合もマーシャリングは自分で面倒を見る必要があります。

マーシャラーを全く介さないと、メソッド呼び出しはダイレクトにCOMインスタンスのメソッドを呼び出す事になります。当然、引数や戻り値に渡されるインターフェイスポインタも生ポインタであるので、アパートメントの境界を越えてアクセスして良いかどうかは全く分かりません。

そのような場合に自分でマーシャリングを実行する必要がありますが、2つの方法があります。

APIやGITを使うと、インターフェイスポインタを再取得したときに、マーシャリングが必要だと判断されれば、マーシャラーが生成されて、マーシャラーへのポインタが返されます。不要と判断されると、直接COMコンポーネントインスタンスへのポインタが返されます。

# 回り回って元のアパートメントでインターフェイスポインタを取得すると、ちゃんと再計算されて、生ポインタが取得できた、ハズ…

現在ではGITを使う方が簡単です。特にATLを使う場合は、CComGITPtrクラスを使うと、非常に簡単にマーシャリングを行うことが出来ます。GITで言うところの「Cookieデータ」が、APIを使う場合の抽象的なデータに相当します。

何故、FTMやニュートラルというオプションがあるのかと言うと、お互いのCOMコンポーネントが何であるかが確実で、お互いに共謀出来るのであれば、異なるアパートメントであろうが、直接生ポインタを操作しても問題ない(と分かっている)はず、という非常に厳しい制約の元でマーシャリングコストを0にすることも出来る、ということです。つまり、ただでさえ複雑なスレッドモデルを正しく理解する必要があるので、FTMやニュートラルを使う事は、よほどの理由がない限り避けた方が良いと言えます(せっかくCOMのランタイムが色々頑張ってくれる機能を放棄することに近しいです)。

DCOMへの拡張

ここまで見てきたアパートメントとマーシャリングの話が分かれば、「DCOM(Distributed COM)」は、延長上の技術でしかない事がわかります。

  • 今までの話は、プロセス内でのアパートメント境界を超える場合のマーシャリング処理ですが、
  • プロセス間通信にも応用できますね?(アウトプロセスCOMサーバー・サービス)
  • プロセス間通信が出来るなら、マシン間でも通信できるよね?(DCOM)

DCOMとは、要するにそういう事です。勿論、マシン境界をまたぐ場合は、セキュリティのトピック(認証と認可をどうするかなど)が存在しますが、基本は全く変わりません。


COMの将来展望(クロージング)

WP_20151219_18_26_52_Pro_LI時間が押してきたので、ほかにもトピックはあるのですがクロージングに入りました。

今回、主題として「All about COM」と銘打ったのですが、実は「隠された副題」がありました。それは、「COM requiem」です。COMの全盛期は2000年頃(つまりもう15年も前)と思っているのですが、まだまだいたるところで使われており、すぐにomitすることは当面出来そうにありません。しかし、プログラミング環境は完全に.NETが主体となり、C++でコードを書く際も、ライブラリのインフラとしてCOMを選択すると言う事は殆ど無くなりました。

自分なりの答えはあったのですが、「何故COMは失敗したのか」あるいは「何故COMは継続してメジャー足り得なかったのか」と言う事を、参加者に聞いてみたかったのです。

意見としては:

  • 設計思想は凄かった。特に「コンポーネント指向」として、処理系依存の排除やバイナリ互換性を維持する方法論、スレッド安全性をランタイムで担保する抽象性の高さ。
  • 「インターフェイス指向設計」に根差している。COMは実態のクラスに一切タッチしないので、インターフェイス分割設計と言う事に強く意識させられた。
  • やっぱり複雑すぎた。時代が早すぎた。複雑性を軽減する開発環境の補助も、現在と比べて全く不足していた。
  • .NETで複雑な処理も簡単に実装できるようになったので、相対的にCOMのランタイムが複雑に見えるようになった。
  • アパートメントという抽象性の高い概念が理解できなくて躓く。
  • COMを正しく実装するのが大変。VB6か、スクリプトコンポーネント(VBSやJSをCOMコンポーネントとして実行可能な、今考えるとやはり早すぎた感のある技術)でないと辛い。

これだけ複雑であるにも関わらず、COMを知っていて良かったと思える事として、やはり「インターフェイス」を強く意識する事になったことが、数人の意見で一致しました。この経験が、現在の.NETのインターフェイスや、ウェブシステムでのAPI設計の粒度やさじ加減と言った点にとても役立っています。

これは、COMのインターフェイスがほぼ最小のメソッドレベルの粒度で公開可能で、しかもDCOMによってマシン間のRPC呼び出しへの簡単にスケールアップ出来るにも関わらず、その細かい(Chatty)呼び出しが、システムのパフォーマンスと安定性に問題を起こす事が経験として理解できたと言う事です。

なので、COMはとても良い経験を与えてくれた良き技術・通過点でした。そろそろ卒業の頃だと思います。

個人的な思い

このブログでも、COMの解説を試みた記事を書いています(未完)。

COMのアパートメント (1) スレッド同期の隠蔽
COMのアパートメント (2) コンポーネントファサード
COMのアパートメント (3) スレッドの属性とコンポーネント
COMのアパートメント (4) スレッド親和性
COMのアパートメント (5) CoInitializeExは初期化ではない
COMのアパートメント (6) アパートメントの種類はどのように決まるのか

これが未完となっていたのと、内容としてこなれていないのが、ずっと喉の奥に引っかかっていたのです。今回のChalkTalkで完全に咀嚼して、完了させたかった。その目的は達成できたかなと。やってよかったなと思います。

COMは愛すべき対象でしたね。「ナナリィ…」って感じです。

次回もよろしくお願いします!


追記

ここで扱っていない、COMの重要な要素として以下のものがあります。メモとして残しておきます。

  • インターフェイスの集約(Aggregation)
  • IDispatchインターフェイス・スクリプティングサポート・イベントソースインターフェイス
  • モジュール生存期間の管理(ATLを使うなら、自動でやってくれる)
  • フリースレッドマーシャラー(FTM・FTMを集約すると、ニュートラルスレッドモデルと同じように振る舞える)
  • モニカ(IMoniker)
  • 情報管理(IStorage・IStream・IPropertyBag)
  • インターフェイスポインタのキャッシュや動的生成
  • ActiveX Controlとは
  • COM+とステートレス設計

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

ずいぶんご無沙汰になってしまった。未だに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で操作する機会が(一般的には)あまり無いからだ。必要なければ、アパートメントを設定する必要もない。


最新のCOM記事:

COMについて、ローカルディスカッションで大幅に拡充してまとめ上げた記事があるので、そちらもどうぞ:「ChalkTalk CLR – COMのすべて」

COMのアパートメント (5) CoInitializeExは初期化ではない

さて、そろそろ架空のコードでは現実とどう違うのかわからなくなってくるので、現実のコードがどうなるのかを見ていくことにする。
COMで最初にお世話になるのは、CoInitializeとCoInitializeEx、CoUninitialize APIだ。
前回までの解説からなんとなくイメージを持ってもらえたかも知れないが、これらのAPIは、現在のスレッドにアパートメント属性を設定・解除する。
CoInitializeはCoInitializeExのバリアントなので、CoInitializeExで説明する。
CoInitailizeEx API

HRESULT hr = CoInitializeEx(0, COINIT_MULTITHREADED);

// COMに関する様々な処理...

CoUninitialize();

CoinitializeExの第一引数は使われていないので0(ないしNULL)を指定する。
第二引数は、初期化フラグを指定し、COINIT列挙値の値を使用する。ほとんどの場合、ここにはCOINIT_MULTITHREADEDかCOINIT_APARTMENTTHREADEDのどちらかを指定する。
COINIT_MULTITHREADEDにした場合、現在のスレッドのアパートメント属性として、MTA(マルチスレッドアパートメント)に属するように設定する。
COINIT_APARTMENTTHREADEDにした場合、STA(シングルスレッドアパートメント)に属するように設定する(そして、CoInitializeを使用すると、STA固定となる)。

マルチスレッドアパートメントに設定すると、そのスレッドは、COMコンポーネントとのやり取りで発生するすべてのスレッド同期作業を、自分で行うと宣言したことになる。
シングルスレッドアパートメントに設定すると、そのスレッドは、COMコンポーネントとのやり取りで発生するすべてのスレッド同期作業を、COMが背後で面倒見てほしいと宣言したことになる。

これらの違いについては次回に譲る。

これでCOMに関する下地が出来た訳だが、巷のコードを見ているとアパートメント選択以前の勘違いが多いことに気が付く。つまり、このAPIの呼び出しは、呼び出し元のスレッド(カレントスレッド)に対してアパートメントの設定を行うのであるが、まるでプロセスワイドにCOMの初期化が行われているかのような使い方だ。

たとえば、DLLによるライブラリを作成していたとする。このDLLは外部に以下のような公開エントリポイントを持つとする。

__declspec(dllexport) HRESULT __stdcall InitializeLibrary();
__declspec(dllexport) HRESULT __stdcall DoSpecialMakeMoney(const ULONG multiply);
__declspec(dllexport) HRESULT __stdcall TerminateLibrary();

このライブラリを初期化するには、InitializeLibraryを呼び出す。内部の実装(DoSpecialMakeMoney)では別のCOMコンポーネントを呼び出す予定なので、InitializeLibrary内でCoInitializeExを呼び出している。しかし、InitializeLibraryを呼び出したスレッドと、DoSpecialMakeMoneyを呼び出したスレッドは別のスレッドかも知れない。同じく、TerminateLibraryもまた、別のスレッドである可能性がある。

すると、CoInitializeExやCoUninitializeを呼び出す事は何の意味をも持たなくなってしまう(CoInitializeExは、「現在のスレッド」にアパートメントを「設定」するのだから)。

それどころか、InitializeLibraryを呼び出した時点で、すでにCOMの初期化が行われているかもしれない。しかもその時点のスレッドは、STAであるべきか、MTAであるべきかは、呼び出し元のみぞ知るわけであり、ライブラリの中では知りようがない(無理やり変更を試みると、RPC_E_CHANGED_MODEが返される)。

仮に、InitializeLibrary内でCoInitializeExのエラーコードを適切にハンドリングしたとする(RPC_E_CHANGED_MODEのほかにS_FALSEが返される可能性がある。これはすでに同じアパートメント属性で初期化されていることを示す)。これでエラーは回避できるかもしれないが、そもそも呼び出し元のスレッドはCOMの初期化を行っておらず、InitializeLibrary呼び出しの「後」で、自力でCoInitializeExを呼び出していたとしたらどうだろうか。その実装には関与できないかもしれず、従ってここで示したようなエラーの回避が行われているかどうかわからず、結局のところ、必要とされる正しいアパートメント属性に設定できたのかどうかもあやふやだ。

つまり、このようなコードは完全な「お手付き」と言える。最も、COMの初期化を行ってあげようという親切心は分からなくもないが…

堅牢なライブラリに仕上げるためには、まず、独自にCoInitializeEx(とCoUninitialize)の呼び出す事を止めることだ。また、CoInitializeExが呼び出されていない場合は、DoSpecialMakeMoneyの呼び出しに対して、COMの初期化が行われていない事を示すCO_E_NOTINITIALIZEDを返す(あるいはカスタムのエラーコードや、C++かつ環境限定なら例外をスローしても良い)。

CoInitializeExが呼び出されているかどうかは、COMの何らかのAPIを呼び出せばわかる。つまり、DoSpecialMakeMoneyの内部で実際にCOM APIを使用した時点でCO_E_NOTINITIALIZEDを検出し、異常処理を行えばよい。
また、呼び出し元のコードを記述する際は、スレッドを生成したら、速やかにCoInitializeExでアパートメント属性を確定しなければならない。生成したスレッドがどのアパートメントに属するのが適切なのかは、スレッドを生成した者にしかわからないからだ。

#筆者は過去にこのような不適切な実装を行っている、サードパーティ製のコンポーネントを目撃した。
#実際に業務に使用していたため、非常に困り、最終的にはサードパーティに「直させた」経験があるが、問題が解消するまでに、結局3か月近く掛かった…
#(技術的な問題点をお客様に理解してもらい、さらにサードパーティベンダーにも周知してもらい、修正され、解消されたことを確認するまでに掛かった時間)
#ベンダーは技術の内容をよく理解して使ってほしいと思う。

.NETのメインエントリポイントが、[STAThread]や[MTAThread]属性を使ってアパートメントを簡単に指定できるようにしているのは、このような理由によるものと推測している。実際、Windows FormsやWPFアプリケーションをウィザードで生成すると、自動的に[STAThread]が適用される。

勿論、属性を指定していなくても、Thread.CurrentThread.SetApartmentState()を呼び出せば設定できる(内部ではおそらくCoInitializeExを呼び出しているのであろう)が、アパートメント初期化のベストプラクティスは、スレッド生成と同時に設定してしまうというものだ。


最新のCOM記事:

COMについて、ローカルディスカッションで大幅に拡充してまとめ上げた記事があるので、そちらもどうぞ:「ChalkTalk CLR – COMのすべて」

COMのアパートメント (4) スレッド親和性

前回までの内容で、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記事:

COMについて、ローカルディスカッションで大幅に拡充してまとめ上げた記事があるので、そちらもどうぞ:「ChalkTalk CLR – 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) スレッド同期の隠蔽

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を使うという事になる。

これをどうやって解決するのかということの、頭をひねった結果が「アパートメント」という考え方だ。
つづく。


最新のCOM記事:

COMについて、ローカルディスカッションで大幅に拡充してまとめ上げた記事があるので、そちらもどうぞ:「ChalkTalk CLR – COMのすべて」