@synchronized による排他制御を行う : Objective-C プログラミング

PROGRAM


@synchronized による排他制御を行う

Objective-C には @synchronized という、複数スレッドからの同時アクセスをブロックする排他制御を行う仕組みが用意されています。

この @synchronized ディレクティブでは、引数に指定した Objective-C インスタンスをキーにして、他からの同時アクセスをブロックしたり、他がブロックを解除されるのを待ったりできます。

 

これと同等の排他制御として ミューテックス があります。

他にも セマフォ@property の atomic キーワードNSLockNSRecursiveLock などが利用できます。

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

同時アクセスされたくない個所を、キーとして使う Objective-C インスタンスを引数にして @synchronized で括ります。

引数に渡すキーには self も使えるので、クラスインスタンス全体で 1 つのブロックで済む場合には、事前の仕様準備をしなくてもすぐに使用できるのが、@synchronized の手軽さです。

- (NSArray*)objectsConformedProtocolTo:(Protocol*)protocol

{

NSMutableArray* result = [[NSMutableArray alloc] init];

 

// self をキーにしてロックします。どこかで self をキーにしたロックがされている場合は、ロックが解かれるまでここで待ちます。

@synchronized (self)

{

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

for (id<NSObject> object in _mutableObjects)

{

if ([object conformsToProtocol:protocol])

{

[result addObject:object];

}

}

}

// スコープを抜けるとロックが解除されます。

 

return [result copy];

}

- (void)removeObject:(id)value

{

// self をキーにしてロックします。どこかで self をキーにしたロックがされている場合は、ロックが解かれるまでここで待ちます。

@synchronized (self)

{

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

[_mutableObjects removeObject:value];

}

// スコープを抜けるとロックが解除されます。

}

これで -objectsConformedProtocolTo: メソッドで配列を走査している最中に removeObject: で配列の内容が削除されてしまうことを防ぎます。

@synchronized のロックの特徴

なお、@synchronized は、同一スレッドで同じインスタンスをキーにして実行したときにはロックされません。

そのため、@synchronized (self) でロックしているスコープの中で呼び出したメソッドが、内部で @synchronized (self) によるロックをしていた場合でも、そのロックは通過します。

 

同じスレッドで実行している限り、それらは同時に処理されることはないので、そういった細やかな気を遣わなくて済むのが利点です。

もちろんその間、別スレッドの @synchronized (self) はブロックしてくれるので、整合性が狂うことはありません。

キーに指定するインスタンスについて

@synchronized では、キーとして Objective-C インスタンスが使用できるので、この例のように _mutableObjects インスタンスに関する処理であれば @synchronized (_mutableObjects) でも大丈夫です。

 

ただし、もし _mutableObjects に設定されているインスタンスが、内部で @synchronized (self) を使用するクラスだった場合、その内部で使用されている @synchronized (self) と、今回の例で使用した @synchronized (_mutableObjects) は同じロックを意味します。

その場合、@synchronized (self) と @synchronized (_mutableObjects) は互いをブロックすることになるので、それを頭に入れてプログラムを組むようにします。

初期化と最終処理

@synchronized で使うキーがいくつか必要な場合について考えてみます。

排他制御に関係するインスタンス変数が Objective-C インスタンスであれば、それをそのまま @synchronized の引数として使用できる場合も多いと思います。

何らかの都合でそれをキーに使えない場合や、NSInteger などのプリミティブ型や構造体などで @synchronized の引数として指定できない場合には、ロック用のインスタンスとして NSObject を用意する方法があります。

 

たとえば、ロックでオブジェクトを 1 つ使いたい場合には、まずは NSObject 型のインスタンス変数を用意します。

@implementation MyClass

{

// ロック時にキーとして指定するインスタンスを格納する場所を用意します。

NSObject* _objectForLock;

}

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

- (id)init

{

self = [super init];

 

if (self)

{

// ロック時にキーとして指定するインスタンスを準備します。

_objectForLock = [[NSObject alloc] init];

}

 

return self;

}

ARC 環境であれば、Objective-C インスタンスは不要になるとリリースされるので、後始末は必要ありません。

ARC 環境ではない場合は -dealloc メソッドなどで、ロック用に確保したインスタンスを release するようにします。

 

後はこれを、ロックしたい場面で @synchronized の引数として使用します。

@synchronized (_objectForLock)

{

 

}

これで、@synchronized の引数にこのオブジェクトを指定してある範囲が、複数個所で同時に実行されることはなくなりました。

@synchronized の使いどころ

ただ、このようにわざわざ NSObject と @synchronized を使うのなら、NSRecursiveLock を使ってロックをするという方法もあります。

NSRecursiveLock でも同等の排他ロックができる上、@synchronized を使うときと比べて高速に制御ができる様子でした。

 

@synchronized を使用する価値としては、何よりもソースコードが読みやすくなるところでしょうか。

中括弧でスコープを作れるので見やすいですし、return だろうと例外だろうと、そのスコープを抜けたタイミングでロックを解消してくれるのは便利ですし、安心です。

遅いと言っても、高速なループで連続使用するような場面でなければ支障がでることもないでしょうから、Objective-C 仕様のロックとしては最適な選択肢なのかもしれませんね。

[ もどる ]