マルチスレッド実装時の留意点 : Objective-C プログラミング

PROGRAM


マルチスレッド実装時の留意点

Objective-C では、"performSelectorInBackground:withObject:" メソッドや "NSOperation" クラス、NSThread クラス などを使用して、別スレッドで処理を行うことができるようになっています。

ここでは、別スレッドでの処理を記述するにあたっての留意点に触れてみたいと思います。

autolerease スコープ

Objective-C では、autolerease メソッドを使用して、オブジェクトを自動的に開放する仕組みが用意されています。

autolerease メソッドを使うと、予め用意されたオートリリースプールによってその変数が管理されて、オートリリースプールのスコープを抜ける時に、その管理下にある変数もリリースされる仕組みになっています。

 

ただ、立ち上げたばかりのスレッドでは、オートリリースプールは用意されていないようで、そのまま autolerease の機能をスレッド内で使用すると、スレッドの実行後に次のようなログが記録されてしまう場合があります。

*** __NSAutoreleaseNoPool(): Object 0x469c of class NSCFString autoreleased with no pool in place - just leaking

このようなときには、立ち上げたスレッド内でオートリリースプールを作成してから autorelease の機能を利用するようにします。

- (void)_threadLoop:(id)userInfo

{

// オートリリースプールを生成します。

@autoreleasepool

{

// 以降は、通常通りに autolerease 等を使用してプログラミングして行きます。

 

 

}

// スコープを抜けたタイミングで、内部で autorelease されたインスタンスが解放されます。

}

このように、NSAutoreleasePool のインスタンスを作成して、それを解放するまでの間に、普段通りのプログラミングをしてあげることで、スレッド内で使用した autorelease の機能が適切に処理されるようになりました。

このあたりの詳細については オートリリースプールの使い方と基本 で整理してみたので、こちらも参考にしてみてください。

無限ループを処理する場合

別スレッドで実行したメソッドは、その内容にも依りますが、たとえば特定の状態になるまで無限ループを行うような場合には、次のような感じの実装になると思います。

- (void)_threadLoop:(id)userInfo

{

// オートリリースプールを生成します。

@autoreleasepool

{

// この例では、自身が持っている status プロパティの値が 0 になるまで処理を繰り返すとします。

while (self.status == 0)

{

// ループを抜けるまで不要な変数が溜まり続けないように、オートリリースプールを用意します。

@autoreleasepool

{

// ここで必要な処理を行います。

 

 

 

// 他への負荷を与えすぎないように、都合に合わせて(例えば 20ms などの)処理停止時間を設けます。

[NSThread sleepForTimeInterval:0.02f];

}

// 次のループ処理に入る前に、オートリリースプールのインスタンスが解放されます。

}

}

// スレッド処理を終える前に、オートリリースプールのインスタンスが解放されます。

}

無限ループ内で、autorelease された使用済みオブジェクトが溜まりすぎてしまわないように、無限ループの内側でもオートリリースプールを作成して、ループが 1 回終わるたびに、不要なオブジェクトが解放されるようにしています。

もちろん、ループ内で autorelease を使用しないようにして、オートリリースプールの作成をしないという方法もあります。

 

また、ループ処理が他のスレッドへの負担にならないように、適度な休憩時間を NSThread クラスの sleepForTimeInterval: メソッドを使って設定しています。

基本的にはこのような感じで、別スレッドでの無限ループを実装することになると思います。

別スレッドからの同時アクセスを考慮する

マルチスレッドでプログラミングを行うと、処理中に別のスレッドからの処理が実行される場合があります。

そうなると、タイミングが悪かったりするとたとえば、NSMutableArray 型で保持している要素の合計値を計算している最中に、別のスレッドから、計算済みの合計値(現在計算中)を取得されるといったことも起こり得ます。

 

そういった、処理途中で別の処理を割り込ませない方法として、Objective-C では @synchronized というキーワードが用意されています。

このキーワードにオブジェクトの変数を渡してあげることで、その変数を誰も @synchronized していなければ、その処理が終わるまでの間、他のスレッドが @synchronized したときに、他のスレッドをブロックして待たせることができます。

逆に、他のスレッドで @synchronized されている場合には、他のスレッドでそのオブジェクトが解放されるのを待ってから、処理を行うようになります。

これを使用して制御することで、マルチスレッド時に誤動作を起こさないようにします。

 

例えば、冒頭で挙げたような合計値の計算を行う場合には、次のような実装になるかと思います。

少し不自然な例になりますが、sum というメソッドで items の合計値を value プロパティ用の値に保存して、外部からは value プロパティを参照することで、取得できるようにしてみます。

// items の合計値を計算して value 変数に格納します。

- (void)sum

{

// 合計値の計算中は、自身をブロックすることで、計算途中のデータを取得されないようにします。

@synchronized(self)

{

value = 0;

 

for (NSNumber item in items)

{

value += [item intgerValue];

}

}

}

 

// value プロパティの値を取得します。

- (NSInteger)value

{

// 自身をブロックすることで、計算中であれば計算が終わるのを待ち、そうでなければ取得直前で再計算されるのを防ぎます。

@synchronized(self)

{

return value;

}

}

このように、そのメソッドまたはプロパティが、またはその中で使用するクラス変数が、そのクラス全体の状態に影響する場合には、@synthesize(オブジェクト名) を使用して、処理結果に整合性が採れるようにします。

同じように、内部の状態に影響を与えるようなオブジェクト(例えば、上記の例の NSMutableArray 型の items 変数)は、直接その参照を返すプロパティを用意してしまうと、このような排他制御が行き届かなくなってしまうので、外部からその値を操作する必要性があるときには、場合によっては、自分自身にその値を操作するメソッドを実装して、内部の items インスタンスは外へ公開しないようにするなどの対応も必要になってくるかもしれないですね。

 

ちなみに Objective-C のプロパティには、マルチスレッドでの誤動作を防ぐための "atomic" という属性が用意されています。

ただし、これは上の @synchronized とは全く別の仕組みで、そのプロパティの値を単体で読み書きするときに、根本的に間違えないことを保護するものになります。これについては @property の atomic キーワードについて の方で詳しく見て行くことにします。

ともあれ @synchronized のように 1 つのオブジェクトを複数の場所で扱うときの整合性を保つものとは別物なので、分けて理解する必要があります。

[ もどる ]