EternalWindows
アパートメント / STAとMTA

COMにおけるクライアントとサーバー間の通信では、当事者の知らないところで数多くの複雑な処理が行われています。 たとえば、COMのクライアントはサーバーがDLLとして実装されていようと、 EXEとして実装されていようとほぼ同じコードを記述できますが、 これはCOMが通信に必要な処理を内部的に行っているからです。 また、サーバーのオブジェクトを実装する際には、 マルチスレッドによる同時アクセスを防ぎたい場合がありますが、 これもそうした旨をレジストリに登録していれば、 COMによって適切な調整が行われます。 つまり、単一のスレッドだけオブジェクトにアクセスすることが保障されることになります。 今回は、こうした事がどのような方法で可能になっているかを考えるために、 アパートメント(以下、アパート)について焦点を当てます。

オブジェクトが単一のスレッドのアクセスだけを想定しているか、 あるいはマルチスレッドのアクセスに対応しているかを明示するために、 オブジェクトはSTA(Single-Threaded Apartment)かMTA(Multithreaded Apartment)かのどちらに属さなければなりません。 オブジェクトがSTAというアパートに属するということは、 そのオブジェクトにアクセスできるスレッドが1つだけということであり、 これによりオブジェクトはデータの同期を意識する必要はなくなりますし、 スレッドのTLSにデータを格納することもできます。 一方、オブジェクトがMTAというアパートに属するということは、 そのオブジェクトにアクセスできるスレッドが複数存在する可能性があり、 オブジェクトはデータの同期が必須になります。 たとえば、オブジェクトの参照カウントを増加させる際には、 InterlockedIncrementを呼び出して変数の同時アクセスを防がなければなりません。 オブジェクトがどのアパートに属することになるかは、 オブジェクトのCLSIDキー以下を参照すれば分かります。

上図は、CLSID_ShellLinkで識別できるオブジェクトのキーを開いているところです。 ThreadingModelというエントリには、オブジェクトが属したいアパートを指定することができ、 これがApartmentである場合はオブジェクトがSTAに属することを意味します。 つまり、このCLSID_ShellLinkで識別できるオブジェクトはマルチスレッドによるアクセスをサポートせず、 シングルスレッドからのアクセスだけをサポートしていることを意味します。 よって、次のようなコードならば問題ないことになります。

#include <windows.h>
#include <shlobj.h>

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	HRESULT      hr;
	IShellLink   *pShellLink;
	IPersistFile *pPersistFile;
	
	CoInitialize(NULL);

	hr = CoCreateInstance(CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pShellLink));
	if (FAILED(hr)) {
		CoUninitialize();
		return 0;
	}

	pShellLink->SetPath(L"C:\\sample.txt"); // リンク先を設定する。
	pShellLink->QueryInterface(IID_PPV_ARGS(&pPersistFile)); // IShellLinkには保存のメソッドがないため、IPersistFileを取得。
	pShellLink->Release();
	
	pPersistFile->Save(L"C:\\sample.lnk", TRUE); // ショートカットファイルを保存。
	pPersistFile->Release();

	CoUninitialize();
	
	return 0;
}

このコードは、オブジェクトの作成から使用までを単一のスレッドで行っているため、 オブジェクトからすれば自身が想定している通りの使い方がされていることになります。 これとは対照的に、問題のあるオブジェクトの使い方を次に示します。

#include <windows.h>
#include <shlobj.h>

DWORD WINAPI ThreadProc(LPVOID lpParameter);

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	HRESULT      hr;
	HANDLE       hThread;
	IShellLink   *pShellLink;
	IPersistFile *pPersistFile;
	
	CoInitialize(NULL);

	hr = CoCreateInstance(CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pShellLink));
	if (FAILED(hr)) {
		CoUninitialize();
		return 0;
	}

	pShellLink->SetPath(L"C:\\sample.txt");
	pShellLink->QueryInterface(IID_PPV_ARGS(&pPersistFile));
	pShellLink->Release();
	
	hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)ThreadProc, pPersistFile, 0, NULL);
	MessageBox(NULL, TEXT("ボタンを押すと終了します。"), TEXT("OK"), MB_OK);
	CloseHandle(hThread);

	pPersistFile->Release();

	CoUninitialize();
	
	return 0;
}

DWORD WINAPI ThreadProc(LPVOID lpParameter)
{
	IPersistFile *pPersistFile = (IPersistFile *)lpParameter;
	
	CoInitialize(NULL); // CoInitializeはスレッド単位で呼び出す。

	pPersistFile->AddRef(); // これからIPersistFileを使用するから参照カウントを上げる。
	pPersistFile->Save(L"C:\\sample.lnk", TRUE);
	pPersistFile->Release();
	
	CoUninitialize();

	return 0;
}

このコードでは、ショートカットの設定はメインスレッドで行っていますが、 ショートカットの保存は別スレッドで行っています。 これはつまり、オブジェクトにアクセスするスレッドが複数存在することを意味し、 ThreadingModelとしてApartmentを指定しているCLSID_ShellLinkの場合では問題であるといえます。

オブジェクトがSTAに属するからといって、マルチスレッドの設計ができなくなるのは面白くありません。 そもそも、このようなアパートの事情はCOMによって隠蔽されてほしい部分ですから、 オブジェクトがどのようなアパートに属していても問題なくアクセスしたいものです。 これを可能にするには、スレッドがオブジェクトに単一でアクセスするのか、 それともマルチスレッドの1つとしてアクセスしたいのかを、COMに伝えなければなりません。 これによってCOMは、スレッドとオブジェクトのどちらの言い分も考慮して、適切なアクセスを提供できるようになります。 スレッドがCoInitializeまたはCoInitializeEx(COINIT_APARTMENTTHREADED)を呼び出した場合、 COMはスレッドがオブジェクトを単一でアクセスしたいと思っていることを認識し、 そのスレッドのためにSTAを作成します。 そして、スレッドはこのSTAの中に属することになります。 この属するというのは、図で表すと次のようなイメージになります。

上図のように、COMを使用するスレッドは何らかのアパートに属することになります。 スレッドがCoCreateInstanceを呼び出した場合、レジストキーを基にオブジェクトが属するアパートも決定することになりますが、 先に示したCLSID_ShellLinkではこれがApartmentになっていました。 つまり、オブジェクトはSTAに属するということになるため、STAの中は次のようになります。

スレッドとオブジェクトが同じアパートに属していることは、非常に望ましいことであることを理解してください。 この場合、スレッドはオブジェクトのメソッドを直接呼び出すことができるため、 後述するようなプロキシ/スタブが内部で使用されることはありません。 また、このSTAに対して別のスレッドが属することは決してないということも重要です。 このような事が可能になっては、オブジェクトにアクセスできるスレッドが複数ということになるため、 スレッドがCoInitializeまたはCoInitializeEx(COINIT_APARTMENTTHREADED)を呼び出した場合は、 常に新しいSTAが作成されてそこにスレッドが属することになります。

スレッドがCoInitializeEx(COINIT_MULTITHREADED)を呼び出した場合は、スレッドがMTAに属することになります。 MTAはプロセス単位で1つだけ存在するため、CoInitializeEx(COINIT_MULTITHREADED)を呼び出す度に新しく作成されるようなことはありません。 先に示した問題のあるコードにおいて、スレッドがCoInitializeEx(COINIT_MULTITHREADED)を呼ぶようになったと仮定して話を進めます。

#include <windows.h>
#include <shlobj.h>

DWORD WINAPI ThreadProc(LPVOID lpParameter);

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	HRESULT      hr;
	HANDLE       hThread;
	IShellLink   *pShellLink;
	IPersistFile *pPersistFile;
	
	CoInitializeEx(NULL, COINIT_MULTITHREADED);

	hr = CoCreateInstance(CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pShellLink));
	if (FAILED(hr)) {
		CoUninitialize();
		return 0;
	}

	pShellLink->SetPath(L"C:\\sample.txt");
	pShellLink->QueryInterface(IID_PPV_ARGS(&pPersistFile));
	pShellLink->Release();
	
	hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)ThreadProc, pPersistFile, 0, NULL);
	MessageBox(NULL, TEXT("ボタンを押すと終了します。"), TEXT("OK"), MB_OK);
	CloseHandle(hThread);

	pPersistFile->Release();

	CoUninitialize();
	
	return 0;
}

DWORD WINAPI ThreadProc(LPVOID lpParameter)
{
	IPersistFile *pPersistFile = (IPersistFile *)lpParameter;
	
	CoInitializeEx(NULL, COINIT_MULTITHREADED);

	pPersistFile->AddRef();
	pPersistFile->Save(L"C:\\sample.lnk", TRUE);
	pPersistFile->Release();
	
	CoUninitialize();

	return 0;
}

見て分かるように、スレッドはCoInitializeEx(COINIT_MULTITHREADED)を呼び出しているため、 これらのスレッドはMTAに属することになります。 これらとオブジェクトの関係を示すと次のようになります。

1つのSTAに属するスレッドは1つだけですが、MTAには複数のスレッドが属することができるため、上図のようになっています。 既に述べたように、スレッドとオブジェクトは同じアパートに属したほうが望ましいのですが、 このオブジェクトをMTAの中に作成することはできません。 このオブジェクトはSTAに属することを望んでいるため、オブジェクトがSTAに属することは必ず保障しなければならないわけです。 このSTAが作成される際にはCOMによってスレッドも作成され、このスレッドがSTAに属してオブジェクトにアクセスすることになります。 こうした状況において、MTAに属するスレッドがどのようにSTAのオブジェクトにアクセスするのかというと、 プロキシというオブジェクトが使用されます。

MTAのスレッドがApartmentのオブジェクトを作成した場合は、オブジェクトの実際のアドレスが返ることはありません。 返されるのは、COMの内部で作成されたプロキシというオブジェクトであり、 スレッドはこのプロキシのメソッドを呼び出すことになります。 この呼び出しを受けたプロキシはスタブにデータを送信し、 このデータを受け取ったスタブはスレッドにデータを渡します。 そして、スレッドがオブジェクトのメソッドを呼び出すことになります。 この流れの最大のポイントは、STAのオブジェクトのメソッドをSTAのスレッドに呼び出させるという点であり、 そのような要求をMTAから伝えるためにプロキシとスタブというオブジェクトが使用されています。 プロキシを通じたメソッド呼び出しは、メソッドの直接呼び出しと比べて処理は遅くなりますが、 アパートを超えたアクセスを提供できていることを考えると、特に気にすることでもありません。

アパートの要点は、スレッドとオブジェクトが同じアパートに属している場合にメソッドの直接呼び出しができるという点と、 同じアパートに属していない場合はプロキシを通じた呼び出しになるという点です。 つまり、スレッドとオブジェクトが同じSTAに属している場合、またはスレッドとオブジェクトが共にMTAに属している場合は、 プロキシを介さずにメソッドを直接呼び出すことができます。 一方、スレッドがMTAに属してオブジェクトがSTAに属する場合は、プロキシを通じた呼び出しになります。 オブジェクトがSTAに属するということは、自分にアクセスするスレッドが単一のスレッドだけでないと困るということですから、 COMによって作成されたスレッドにそれを任せるためにプロキシを使用します。 それでは、スレッドがSTAに属してオブジェクトがMTAに属する場合はどうなるのでしょうか。 この場合もスレッドに返されるオブジェクトはプロキシのアドレスであり、 プロキシのメソッドを呼び出すことで、COMによって作成されたスレッドがMTAのオブジェクトにアクセスするようになります。 しかし、この場合は本当にプロキシを通じた呼び出しでなければならないのでしょうか。 オブジェクトがMTAに属するということは、自分にアクセスするスレッドが複数でも構わないということですから、 STAに属するスレッドがメソッドを直接呼び出しても問題ないのでしょうか。 実はこれは、オブジェクトがFTM(Free Threaded Marshaler)をサポートすることで可能になります。 オブジェクトのプロキシやスタブが必要になるかは、オブジェクトがQueryInterfaceでIMarshalを返していないかどうで判断されるため、 この際にCoCreateFreeThreadedMarshalerで取得したIMarshalを返すようにします。 そうすると、スレッドにはプロキシではなくオブジェクトの実際のアドレスが返るようになります。

アパートの確認

スレッドが属しているアパートは、IComThreadingInfoによって確認することができます。 次に例を示します。

#include <windows.h>

void ShowThreadApartmentType(LPTSTR lpszTitle);
DWORD WINAPI ThreadProc(LPVOID lpParameter);

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	HANDLE hThread;

	CoInitialize(NULL);
	
	hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)ThreadProc, NULL, 0, NULL);
	ShowThreadApartmentType(TEXT("メインスレッド"));
	CloseHandle(hThread);
	
	CoUninitialize();
	
	return 0;
}

DWORD WINAPI ThreadProc(LPVOID lpParameter)
{
	CoInitialize(NULL);

	ShowThreadApartmentType(TEXT("スレッド"));
	
	CoUninitialize();

	return 0;
}

void ShowThreadApartmentType(LPTSTR lpszTitle)
{
	TCHAR             szBuf[256];
	APTTYPE           apttype;
	IComThreadingInfo *pComThreadingInfo;

	CoGetObjectContext(IID_PPV_ARGS(&pComThreadingInfo));

	pComThreadingInfo->GetCurrentApartmentType(&apttype);
	wsprintf(szBuf, TEXT("%d"), apttype);
	MessageBox(NULL, szBuf, lpszTitle, MB_OK);

	pComThreadingInfo->Release();
}

自作関数のShowThreadApartmentTypeを呼び出すと、呼び出し元スレッドのアパートの種類が表示されます。 0(APTTYPE_STA)の場合はスレッドがSTAに属していることを意味し、3(APTTYPE_MAINSTA)の場合はスレッドがメインSTAに属していることを意味します。 メインSTAは最初に作成されたSTAであり、上記の場合であればメインスレッドが属することになります。 CoInitializeEx(COINIT_MULTITHREADED)を呼び出すと1(APTTYPE_MTA)が表示されることになります。

オブジェクトは、自身が属したいアパートをThreadingModelエントリに指定することができます。 アパートの種類は、次の文字列で識別されます。

ThreadingModel 説明
値なし オブジェクトはメインSTAに属する。
Apartment オブジェクトはSTAに属する。
Free オブジェクトはMTAに属する。
Both オブジェクトはSTAかMTAに属する。
Neutral オブジェクトはNAに属する。

ThreadingModelを指定していない場合は、オブジェクトがメインSTAに属することになるため、 こうしたオブジェクトはメインスレッドが作成するのがよいといえます。 ThreadingModelがBothである場合は、オブジェクトがSTAでもMTAでも実行できることを意味しています。 この場合、オブジェクトを作成したスレッドがSTAの場合はSTAに作成され、MTAである場合はMTAに作成されます。 NA(Neutral Apartment)はWindows 2000から導入されたアパートであり、STAとMTAの両方の特徴を持っています。 NAはMTAと同様にプロセス単位で1つ存在し、そこには複数のスレッドが属することができます。 しかし、それらのスレッドが同時に実行されることはないため、オブジェクトはスレッドセーフに実装されている必要はありません。 この点はSTAと同様ですが、NAはSTAのようにオブジェクトにアクセスするスレッドが常に同一であることを想定していません。 つまり、同時にアクセスされなければ、どのようなスレッドがオブジェクトにアクセスしても構わないということになります。



戻る