Technote 1122
Locking and Unlocking Handles
基礎概念
ハンドルとはメモリマネージャが管理するメモリブロックの一種です。ハンドルはメモリブロックを間接的に参照(ポインタのポインタ)するため、メモリマネージャは明確に定義された状況下であれば、メモリブロックをメモリ空間内で自由に移動することができます。ハンドルが指すポインタはマスタポインタと呼ばれています。メモリブロックが移動されてもメモリマネージャはマスタポインタを更新するだけで、ハンドルは変わることがありません。(メモリマネージャとハンドルの仕組はInside Macintosh: Memory第1章「Introduction to Memory Management」の「Heap Management」で詳しく解説されています)。
ハンドルの使用方法によっては、ロック(移動されないように固定)する必要があります。メモリを動かすような関数(直接的又は間接的にメモリマネージャの関数を呼び出してメモリを動かす可能性のある関数)にパラメータとしてメモリブロックのアドレスを渡す場合は、そのメモリブロックのハンドルを予めロックしなければなりません。PtrAndHand ()と言うツールボックス関数は簡単な例です。一方、間接的にメモリマネージャを呼び出す関数の例として、NewWindow ()があります。また、メモリマネージャは呼び出さないが、関数を呼び出す行為自体がメモリを動かしてしまうものがあります。例えば、メモリに読み込まれていないコードセグメントリソースに関数が含まれていると、リソースを読み込むことでメモリが移動されることがあります。(コードセグメントの仕組はInside Macintosh: Memory第1章「Introduction to Memory Management」の「Loading Code Segments」とInside Macintosh: Processes第7章「Segment Manager」で詳しく解説されています)。
ハンドルのロックに関する問題
ハンドルにはロックされていることを示すフラグがありますが、メモリマネージャはハンドルのロックされた回数を把握していません。アプリケーションがハンドルを二度ロックしても、ロックを一度解除するだけでハンドルはすぐに解除状態となります。このことから、アルゴリズムがある程度複雑になると、ロックとロックの解除を一致させるだけでは足りません。以下のコードはハンドル管理の悪い例です。
static pascal OSErr Unsafe_DoSomethingToHandle (Handle h)
{
OSErr err = noErr;
HLock (h);
if (!(err = MemError ())) {
err = DoSomethingToPointer (*h);
HUnlock (h); // 危険!
if (!err)
err = MemError ();
}
return err;
}
|
Unsafe_DoSomethingToHandle ()に渡されたハンドルパラメータが呼び出し側のコードによってロックされていた場合でも、Unsafe_DoSomethingToHandle ()はHUnlock ()を実行するため、ハンドルのロックはいつも解除されてしまいます。この結果、Unsafe_DoSomethingToHandle実行後は呼び出し側のコードが痛い目に会う可能性があります。
上記のコードはわかりやすい例ですが、アルゴリズムが複雑になるとデバッグは非常に困難です。ハンドルのロックが保たれることを前提にしているコードはシステムソフトウェアを含めて、問題の原因となっているコードと全然違う所にあるかもしれません。テックノート1118「GDHandleのロック解除の問題について」はシステムソフトウェアがハンドルのロックに依存する一例を紹介しています。
解決方法
一般的な解決方法は関数単位やアルゴリズム単位でハンドルの属性を保存することです。
重要:
いつもハンドルの属性を保存することで上記のような問題は完全に避けられます。ただし、ハンドルの属性を保存しなくても良い場合もありますので、無駄な行為となることがあります。必要な時だけにハンドルの属性を保存したい場合は、保存を必要とする状況を良く理解することが重要です。ハンドルの属性を保存しなくても良い状況はこのテックノートの最後の方で紹介されています。しかし、日頃から安全重視でコードを書いている方はこれらを無視して、常にハンドルの属性を保存するように心がけて下さい。 |
メモリマネージャは各ハンドルについて属性(数ビットのフラグ)を管理しています。このフラグの内一つはロック状況です。ハンドルのロック状況を知るにはHGetState ()を利用します。以下の例はすべてHGetState ()を利用しています。
正しいロック方法その1
もっとも簡単な方法はハンドルの属性を丸ごと保存して、終了時に元に戻す方法です。ハンドルのロック状況を示すフラグは実行前と実行後で同じ値なので、ハンドルのロック状況は保たれます。以下は典型的なコード例です。
static pascal OSErr SafeOne_DoSomethingToHandle (Handle h)
{
OSErr err = noErr;
SInt8 hState = HGetState (h);
if (!(err = MemError ())) {
HLock (h);
if (!(err = MemError ())) {
err = DoSomethingToLockedHandle (h);
HSetState (h,hState);
if (!err)
err = MemError ();
}
}
return err;
}
|
この方法は安全ですが、ハンドルの属性をすべて保つため、DoSomethingToLockedHandle ()がロック状況以外の属性(例えばハンドルのパージ状況)を変更してもHSetState ()で変更が打ち消されてしまいます。
正しいロック方法その2
もう少し高度な方法はハンドルの属性を予め確認して、ロックされていない時だけハンドルのロックとロックの解除を行う方法です。以下は典型的なコード例です。
enum { kHandleLockMask = 0x80 }; // <MacMemory.h> 3.0.1から抜けているため
static pascal OSErr SafeTwo_DoSomethingToHandle (Handle h)
{
OSErr err = noErr;
SInt8 hState = HGetState (h);
if (!(err = MemError ())) {
Boolean handleWasLocked = (kHandleLockMask & hState) ? true : false;
if (!handleWasLocked) {
HLock (h);
err = MemError ();
}
if (!err) {
err = DoSomethingToLockedHandle (h);
if (!handleWasLocked) {
HUnlock (h);
err = MemError ();
}
}
}
return err;
}
|
この方法ではロック状況以外の属性が影響される心配はありませんが、欠点はコード自体の複雑さです。このようなコードはわかりにくい性質があるので、DTSのオタクを除けばこの複雑さは避けたいところでしょう(もちろん、ここで言うDTSとはあくまでも米国本社を指しています)。またハンドルの数が増すとコードを書き間違えてしまう確率が上がるのも事実です。
上記のアルゴリズムをC++のクラスで包んでしまうと複雑さを少し緩和できます。C++クラスのconstructorではハンドルを必要に応じてロックし、destructorで必要に応じてロックを解除します。また、C++のクラスを使うことによってエラーやexceptionが発生した時でもロックが自動的に解除される利点があります。
ハンドルの属性を保存しなくても良い場合
ハンドルの属性を保存しなくても良い場合は不必要なメモリマネージャの利用を控えることで最適化を図ることができます。最適化は必ず十分な計測をしてから行って下さい。また、安定性を損なうとせっかくの最適化も無意味です。
ハンドルをロックしない
ハンドルをロックしなければ、ハンドルの属性を保存する必要も無くなります。これは当り前のようですが、必要もないのにハンドルをロックしているコードが沢山あります。メモリマネージャ(又は他のマネージャの間接的な行為)がメモリブロックを移動するタイミングを理解することで不必要なハンドルのロックが避けられます。例えば、デベロッパ・テクニカル・サポートでは以下のようなコードを良く見かけます。
static pascal OSErr UnnecessaryHandleLock (Handle h)
{
OSErr err = noErr;
SInt8 hState = HGetState (h);
if (!(err = MemError ())) {
HLock (h);
if (!(err = MemError ())) {
**h = 12; // たかがこのためにロックを?
HSetState (h, hState);
if (!err)
err = MemError ();
}
}
return err;
}
|
更に、ハンドルをロックしなくても良い状況を利用するだけに止まらず、ハンドルをロックしなくても良いようにコードを設計することも可能です。例えば、メモリブロックの一部を変更するような場合は変更を一旦ローカル変数に保存して、最終的にメモリブロックに書き移します。以下の例をご覧下さい。
static pascal OSErr AvoidHandleLock (Handle h)
{
OSErr err = noErr;
char c = **h;
err = DoSomethingToCharacter (&c);
if (!err)
**h = c;
return err;
}
|
独占的にハンドルを利用する場合
コードの一部が独占的にハンドルを利用している場合は、他の関数がそのハンドルに頼る心配がないので、自由にロック又はロックの解除が行えます。
例えば、コードの一部が外に見えない内部バッファをハンドルとして持っている場合は、そのコードがハンドルの生成から解放までをすべて管理するため、独占的にハンドルを利用していることになります。
もう一つの例は一時的にバッファを割り当てて、終了直前に解放するコードです。NewHandle ()が生成するハンドルはロックされませんが、DisposeHandle ()はロックされたハンドルも受け付けますので以下のようなアルゴリズムが成立します。
static pascal OSErr SafeThree_DoSomethingToHandle (void)
{
OSErr err = noErr;
Handle h = NewHandle (12);
if (!h)
err = MemError ( );
else
{
HLock (h);
err = MemError ( );
if (!err)
err = DoSomethingToLockedHandle (h);
DisposeHandle (h);
if (!err)
err = MemError ( );
}
return err;
}
|
概要
HLock ()とHUnlock ()だけではハンドルの属性を安全に保てない場合が多いです。ハンドルの属性を保存して、元に戻す必要があります。ハンドルの属性を保存しなくても良い場合もありますが、保存しないと選んだ場合は状況を良く理解した上でコードを書いて下さい。自信がない場合はハンドルの属性を常に保存して、元に戻すのが安全です。
|