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のすべて」