しかし、実装が不完全で限られた使い方でしか動作は保証されていない。
取り分け、シングルトンオブジェクトの排他制御がまともに出来ていないのと、スレッドごとに異なる値を持たなければならない変数が静的変数になっていてスレッド間で共有されているのが課題。
mutexの初期化
オブジェクト固有の mutexは、そのオブジェクトを生成するときに初期化を行えばよい。問題はシングルトンの場合だ。
static boolean initialized = false;
struct Something object;
:
if (!initialized) {
/* object will be initialized. */
}
このようなコードは、単一のスレッドから呼び出す場合にのみ動作が保証される。
複数スレッドから同時に呼ばれる可能性がある場合、initializedのチェックは役に立たない。
そこで、mutexにより排他制御を行って見よう。(pthreadの場合。)
#include
static boolean initialized = false;
static pthread_t mutex;
struct Something object;
:
pthread_mutex_lock(&mutex);
if (!initialized) {
/* object will be initialized. */
}
pthread_mutex_unlock(&mutex);
この処理が呼ばれる度に mutex操作が発生するとオーバヘッドが大きくなるので、それを回避する必要がある場合には外側にもう一段 initializedのチェックを入れると良い。
いづれにしてもこの mutexは事前に初期化されていなければならない。
特定のアプリケーションであれば、mutex(または objectの)初期化が完了してから複数スレッドを起動するように設計すれば十分であるが、ライブラリやフレームワークなどのミドルウェアの場合そのような制約はあてにならない。
そのようなミドルウェアは、mutex(または objectの)初期化を何らかの初期化関数として提供することになるが、仮にアプリケーションが初期化関数を呼んだ後にしかスレッドを起動しなかったとしてもまだ不十分である。
アプリケーションだけでは初期化の順序を守れないケースがあるからである。
2011年のことだったと思う。開発していた組み込み製品のデバッグ担当者から彼らの上司に『main関数にブレークポイントを設定したのに効きません。』とか『main関数よりも前に関数が呼ばれます。』という電話連絡が入った。
組み込みの開発では言語 Cを使用することが多いが、そのプロジェクトは C++が使われていたのだ。
Cでは、静的変数や外部変数を初期化するときに、main関数を呼ぶ startupルーチンの中から 変数領域の初期化が行われる。(bss領域はゼロフィルされ、data領域は初期値がコピーされる。)
ユーザのコードが mainよりも前に走り始めることは無い。
しかし、C++では定数で初期化するだけでなく、コンストラクタを起動したり、関数の結果を初期値に出来る。
#include
#include
std::string message("Hello, world.");
int len = message.length();
int main() {
std::cout << message << std::endl;
std::cout << len << std::endl;
return 0;
}
C++のときだけではない。gccの拡張機能である constructor属性を関数に指定すると、main関数よりも前に実行される。アプリケーションが main関数よりも前に何もしないつもりでも、使用しているライブラリが何かスレッドを起こして、なにかするかも知れない。
pthread_once
pthreadの場合、pthread_onceを使えば、そのプロセスでただ一度だけ処理を実行することが保証される。mutexの初期化を気にせずにシングルトンを実現できる。
#include
struct Something object;
void initializer() {
/* object will be initialized. */
}
:
pthread_once(initializer)
/* use object */
これで簡単確実にオブジェクトを初期化することができるようになった。
PTHREAD_MUTEX_INITIALIZERによる mutexの初期化
多くの場合のオブジェクトの初期化は pthread_onceでうまくいくが、pthread_onceでロックしてしまう場合がある。softiesプロジェクトでは、Objectクラスはは Classクラスのインスタンスを持っていてこれは Objectを使用する前に初期化されなければならない。
その Classクラスは Objectクラスのサブクラスであり、Classクラスを初期化する場合にはそれに先立って Objectクラスを初期化しなければならない。
このように循環している場合、pthread_onceではロックするのである。
勿論、Object用の初期化関数と Class用の初期化関数は分けてあり、pthread_onceは各々別々に呼び出されるが、その中で相手側を呼び出すことになる。 (アプリケーションから明示的にフレームワーク、ライブラリの初期化関数を呼び出すようにしていないため、順序固定で初期化されるようなコーディングは行っていない。)
Objectの pthread_onceで指定された初期化関数の中から、Classの pthread_onceで指定された初期化関数を呼ぶところまでは何の問題もない。 (順序が逆になってもそうだ。)
ところが、その Classの初期化関数から再び Objectの pthread_onceを呼び出すと pthread_onceの呼び出しから復帰しなくなる。
そこで、何とかもう一度 mutexを使う方法を検討してみた。 普段、 mutexを使用するときは pthread_mutex_init関数を使用していた。そうしなければならないと思っていた。
しかし、ドキュメントをよく読むと、PTHREAD_MUTEX_INITIALIZERという初期値があるではないか。
つぎのように定数として mutexを初期化できる。
static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
これなら、main関数よりも前に startupルーチン関連以外の処理が走るよりも前に mutexが使えるようになる為、最初のような initializer変数と組み合わせてオブジェクトの初期化を行うことができる。Classクラスと Objectクラスのような初期化が循環していても、どちらの初期化から先に行おうとしたときでもロックすることなく処理を行うことができるようになった。
ただし、ヒープメモリから割り当てた領域に mutexを配置したり、pthread_mutexattr_tを指定した初期化したい場合には PTHREAD_MUTEX_INITIALIZERは使えない。
数ヶ月前から、電車のディスプレイに中日新聞ニュースが流れないことがしばしばあり、折角μチケット買ったのに残念な思いをする。
0 件のコメント:
コメントを投稿