ミューテックスによる排他制御を行う : Objective-C プログラミング

PROGRAM


ミューテックスによる排他制御を行う

Objective-C では、ミューテックスという複数スレッドからの同時アクセスをブロックする排他制御が利用できます。

ミューテックスとは、それで制御されている区間が「使用中か」「未使用か」を判断するための機構です。

 

ここではそれをつかって、複数スレッドからのアクセスをされたくない場所(クリティカルセクション)を保護する方法について記します。

なお、Objective-C で排他制御を行う方法には、他にも @synchronizedセマフォ@property の atomic キーワードNSLockNSRecursiveLock などがあります。

初期化と最終処理

Objective-C でミューテックスを使用するためには、使用するヘッダーやソースコードで pthread.h をインポートする必要があります。

#import <pthread.h>

これによって、ミューテックスを制御する関数を利用できるようになります。

 

続いて、ミューテックスを利用するために、まずは pthread_mutex_t 型のインスタンス変数を用意します。

@implementation MyClass

{

// ミューテックスを管理する変数を用意します。

pthread_mutex_t _mutex;

}

これを init メソッドなどの、どこか適切な場所で初期化します。

- (id)init

{

self = [super init];

 

if (self)

{

// ミューテックスを管理する変数を使えるようにします。

pthread_mutex_init(&_mutex, NULL);

}

 

return self;

}

このとき、pthread_mutex_init の第 1 引数で指定するポインタが指す pthread_mutex_t の値は 0 で初期化されている必要があります。

Objective-C のインスタンス変数は、クラスインスタンス生成時に 0 で初期化されるので、今回の例のようにインスタンス変数の pthread_mutex_t を使用する場合はそのまま渡せます。

 

ミューテックスを使い終わったときの最終処理も、どこか適切な場所で行います。

- (void)dealloc

{

// ミューテックスを管理する変数を処分します。

pthread_mutex_destroy(&_mutex);

}

pthread_mutex_destroy 実行後は、この変数を使ったミューテックス制御はできなくなります。

クリティカルセクションを保護する

ミューテックスを管理する変数を用意したら、それを使って同時アクセスされたくないところを保護します。

- (struct MyStructs)value

{

struct MyStructs result;

 

// ミューテックスをロックします。どこかでロックされている場合は、ロックが解かれるまでここで待ちます。

pthread_mutex_lock(&_mutex);

 

// 割り込まれたくない処理をここで行います。

result = _value;

 

// ミューテックスのロックを解きます。

pthread_mutex_unlock(&_mutex);

 

return result;

}

- (void)setValue:(struct MyStructs)value

{

// ミューテックスをロックします。どこかでロックされている場合は、ロックが解かれるまでここで待ちます。

pthread_mutex_lock(&_mutex);

 

// 割り込まれたくない処理をここで行います。

_value = value;

 

// ミューテックスのロックを解きます。

pthread_mutex_unlock(&_mutex);

}

たとえばこのようにすることで、-setValue: メソッドで値を書き換えている最中に -value メソッドで中途半端な値を取得されないようにすることができます。

今回の例でミューテックスで保護しているのは、-setValue: でも -value でも、単純な代入文の 1 行ですけど、プログラムの内部では構造体を何回かに分けてコピーしているため、この単純な代入文でも割り込まれると、値が壊れてしまいます。

そのため、複数スレッドから同時アクセスされる可能性がある場合には、このようにロックで保護する必要があります。

ミューテックスのロック方式

ミューテックスを生成する際に使用する pthread_mutex_init 関数では、第 2 引数でロック方式を指定できます。

指定できるロック方式は、次の 3 つがあるようです。

iOS SDK での定義名 pthread での一般的な定義名 ロック方式による pthread_mutex_lock の動作
PTHREAD_MUTEX_NORMAL PTHREAD_MUTEX_FAST_NP 誰かがロックしているときに、それが解放されるまで永遠に待ちます。(同一スレッド内でのロックもブロック、その代り動作が速い)
PTHREAD_MUTEX_RECURSIVE PTHREAD_MUTEX_RECURSIVE_NP 誰かがロックしているときに、それが解放されるまで永遠に待ちます。(同一スレッド内での2度目以降のロックは素通り)
PTHREAD_MUTEX_ERRORCHECK PTHREAD_MUTEX_ERRORCHECK_NP 誰かがロックしているときに、直ちに EDEADLK (11) を戻り値に返します。(同一スレッド内で 2 度目のロックがあったことを検出できる)

第 2 引数で NULL を指定した場合は、PTHREAD_MUTEX_NORMAL が指定されたのと同じになります。

 

これらのロック方式を指定するには、pthread_mutex_init を呼び出すときに、これらの値を pthread_mutexattr_t 型で渡す必要があります。

// pthread_mutexattr_t 変数を用意します。

pthread_mutexattr_t mutexattr;

 

// pthread_mutexattr_t 変数にロック方式を設定します。

pthread_mutexattr_init(&mutexattr);

pthread_mutexattr_settype(&mutexattr, PTHREAD_MUTEX_RECURSIVE);

 

 

// ミューテックスを初期化します。

pthread_mutex_init(&_mutex, &mutexattr);

 

// 不要になった pthread_mutexattr_t 変数を破棄します。

pthread_mutexattr_destroy(&mutexattr);

このようにすることで、ロック方式を指定してミューテックスを初期化できます。

ロック方式を使用するのに使った pthread_mutexattr_t の変数は、ミューテックスを初期化するときに使用する pthread_mutex_init 関数に渡したら、もう破棄しても大丈夫なようです。

pthread_mutex_lock の動作について

pthread_mutex_lock でロックしたとき、その動きはロック方式によって変わってきます。

 

PTHREAD_MUTEX_NORMAL

PTHREAD_MUTEX_NORMAL の場合、同一スレッド内も含めて、どこかでロックされている場合はそれが解放されるまでブロックされます。当然、自分のスレッドの別の場所でロックされている場合、待っていても解放されることがないので、そのままデッドロックに陥ります。

それでも処理スピードが速いので、高速な処理が期待される場面で、自分自身で同じロックを使用するメソッドを呼び出すことがないのが確実なときに限り、使用するのが良さそうです。

そんなデッドロックの危険性をはらんでいますけど、広範囲をひとつのロックで排他制御しているようなことがなければ、PTHREAD_MUTEX_NORMAL でもあまり問題は起きないかもしれません。

 

なお、PTHREAD_MUTEX_NORMAL で、ロックされていないミューテックスをロック解除しようとした場合、動作は未定義だそうです。

Xcode 4.5.2 でビルドした iOS 6.0 アプリで試してみた感じでは、正常終了 (0) になるようでした。

 

PTHREAD_MUTEX_RECURSIVE

PTHREAD_MUTEX_RECURSIVE の場合は、同一スレッドで 2 回目以上のロックが実行された場合に、ブロックされずに通過できます。

同じスレッドがブロックしているのを待つことがないので、デッドロックに陥る可能性はだいぶ少なくなると思います。同一スレッドでのロックかどうかの判定が加わるせいか、PTHREAD_MUTEX_NORMAL よりも速度が遅くなりますが、極端な速度の低下はなさそうです。

 

クラスインスタンスの排他制御など、ロックの中で次から次へとさまざまな機能を呼び出す場面で使いやすいロックタイプかもしれません。

Objective-C の @synchronized ディレクティブも、この PTHREAD_MUTEX_RECURSIVE 方式を採用しています。

 

なお、PTHREAD_MUTEX_RECURSIVE で、ロックされていないミューテックスをロック解除しようとした場合には、EPERM (1) が戻り値として返されるようです。

 

PTHREAD_MUTEX_ERRORCHECK

PTHREAD_MUTEX_ERRORCHECK は、PTHREAD_MUTEX_NORMAL の動作検証に使用できるもののようです。

同一スレッドで 2 度目以上のロックが実行された場合に EDEADLK (11) が返されます。

これにより、同じスレッドで 2 度目のロックがあったかどうかを検証することができます。

 

ただ、あるスレッドでロック中に別のスレッドでロックしようとした場合には、このロック方式であってもロックが解除されるのを待つようです。

そのため、スレッドをまたいだデッドロックの検証まではできないようでした。スレッドをまたいでロックを検証したい場合には、ロック済みの場合に EBUSY (16) を返す pthread_mutex_trylock を使う必要がありそうです。

 

なお、PTHREAD_MUTEX_ERRORCHECK で、ロックされていないミューテックスをロック解除しようとした場合には、EPERM (1) が戻り値として返されるようです。

pthread_mutex_trylock の動作について

pthread_mutex_trylock の動作についても、先ほどの pthread_mutex_lock とほとんど同じようです。

違いは、pthread_mutex_trylock の場合は、指定されたミューテックスがロックされていた場合には EBUSY (16) を返すという点です。

 

こちらの場合も、PTHREAD_MUTEX_NORMAL であれば、同じスレッドの別の場所でロックされている場合も EBUSY (16) が返されるようです。

PTHREAD_MUTEX_RECURSIVE の場合には、同じスレッドの別の場所でロックがされている場合には 0 (ロック完了)が返されました。

ミューテックスの初期化の種類

ミューテックスの初期化には、大きく分けて 2 つの方法があります。

  1. pthread_mutex_init 関数を使用する方法
  2. PTHREAD_MUTEX_INITIALIZER, PTHREAD_RECURSIVE_MUTEX_INITIALIZER, PTHREAD_ERRORCHECK_MUTEX_INITIALIZER を使用する方法

最初の方は、今回の例で使用した生成方法ですね。

後者も、pthread_mutex_init のように使用できるのですが、"ORACLE 相互排他ロックの使用方法(マルチスレッドのプログラミング) - pthread_mutex_init(3T)" によると、静的変数で使用できるとあります。

その辺りのヒントになりそうな情報が "IT 戦記 - pthread でキューを作ってみる(再々挑戦、最終版) - pthread_mutex_init と PTHREAD_MUTEX_INITIALIZER" から読み取れました。

 

これによると、どうやら pthread_mutex_init が直ちに pthread_mutex_t 変数を初期化するのに対し、PTHREAD_MUTEX_INITIALIZER ではメモリを初期値に埋めるだけで、最初のロックのときに初めて pthread_mutex_t が phread_mutex_init で初期化されるのだそうです。

つまり、static 宣言のところでできない初期化を、PTHREAD_MUTEX_INITIALIZER では後に回しているのでしょう。

 

ちなみに、PTHREAD_MUTEX_INITIALIZER 系の #define の違いは、ミューテックスのロック方式の違いになります。

定義名 対応するロック方式
PTHREAD_MUTEX_INITIALIZER PTHREAD_MUTEX_NORMAL
PTHREAD_RECURSIVE_MUTEX_INITIALIZER PTHREAD_MUTEX_RECURSIVE
PTHREAD_ERRORCHECK_MUTEX_INITIALIZER PTHREAD_MUTEX_ERRORCHECK

静的変数として pthread_mutex_t を定義した場合に、定義と併せてこれを代入しておけば、すぐに pthread_mutex_lock を使い始められるのは便利ですね。

[ もどる ]