同期化
アプリケーション内に複数のスレッドが存在すると、複数の実行スレッドからのリソースへの安全なアクセスに関する潜在的な問題が発生します。同じリソースを変更する 2 つのスレッドが意図しない方法で互いに干渉する可能性があります。たとえば、あるスレッドが別のスレッドの変更を上書きしたり、アプリケーションを不明で潜在的に無効な状態にすることがありえます。あなたが幸運であれば、破損したリソースは明らかにパフォーマンス上の問題やクラッシュは追跡や修正が比較的容易です。しかし、あなたが不運であれば、破損によって、あまりにも遅くまで明示されない微妙なエラーが発生する可能性があります。また、エラーによって、基礎となるコーディング仮定を大幅に見直す必要があります。
スレッドの安全性に関して言えば、優れた設計が最良の保護です。共有リソースを避け、スレッド間のやりとりを最小限に抑えると、それらのスレッドが互いに干渉する可能性が低くなります。しかし、完全に干渉のない設計は常に可能ではありません。スレッドが対話しなければならない場合は、同期ツールを使用して対話するときに安全に実行する必要があります。
OS X と iOS には、相互に排他的なアクセスを提供するツールから、アプリケーションでイベントを正しく配列するツールまで、さまざまな同期ツールが用意されています。以下のセクションでは、これらのツールについて説明し、コード内であなたがこれらのツールを使用してプログラムのリソースへの安全なアクセスに影響を与える方法について説明します。
同期ツール
異なるスレッドが予期せずデータを変更するのを防ぐには、アプリケーションに同期の問題がないように設計するか、同期ツールを使用できます。同期の問題を避けることは望ましいですが、必ずしも可能であるとは限らりません。以下のセクションでは、使用可能な同期ツールの基本カテゴリについて説明します。
アトミック (微少) な操作
微少な操作はシンプルな形式の同期で、単純なデータ型で動作します。微少な操作の利点は、競合するスレッドをブロックしないことです。カウンタ変数を増分するなどの簡単な操作では、ロックを取るよりもはるかに優れたパフォーマンスが得られます。
OS X および iOS には、32 ビットおよび 64 ビットの値に対する基本的な数学的および論理的演算を実行するための多数の演算が含まれています。これらの演算の中には、比較し交換、テストし設定、テストしクリア操作のアトミックバージョンがあります。サポートされているアトミック操作のリストについては、/usr/include/libkern/OSAtomic.h ヘッダーファイルを参照するか、atomic のマニュアルページを参照してください。
メモリーバリアと揮発性変数
最適なパフォーマンスを達成するために、コンパイラはしばしばアセンブリレベルの命令を並べ替え、プロセッサの命令パイプラインを可能な限りフルに保ちます。この最適化の一環として、コンパイラは、メインメモリにアクセスする命令を並べ替えると、間違ったデータを生成しないと考えられます。残念ながら、コンパイラがすべてのメモリ依存の操作を検出することは、必ずしも可能ではありません。一見異なる変数が実際には互いに影響する場合、コンパイラの最適化は間違った順序で変数を更新し、誤った結果を生じる可能性があります。
メモリーバリアは、メモリ操作が正しい順序で行われることを保証するために使用されるブロックしない同期ツールの一種です。メモリーバリアはフェンスのように動作し、バリアの後ろに配置されたロードおよび保管操作を実行する前に、バリアの前に配置されたロードおよび保管操作をプロセッサに完了させます。メモリーバリアは、通常、あるスレッド (しかし別のスレッドから見えます) によるメモリ操作が常に予期された順序で行われるようにするために使用されます。このような状況でメモリバリアがないと、他のスレッドが一見不可能な結果と見える可能性があります。(例えば、メモリーバリア に関する Wikipedia のエントリを参照してください) メモリバリアを使用するには、コード内の適切な場所で OSMemoryBarrier 関数を単に呼び出して下さい。
ロック(Locks)
ロック (Locks) は、最も一般的に使用される同期ツールの 1 つです。ロックを使用すると、一度に 1 つのスレッドのみがアクセスを許可されるコードのセグメントであるコードの 重要なセクション を保護できます。たとえば、重要なセクションでは、特定のデータ構造体を操作したり、一度に最大で 1 つのクライアントをサポートするリソースを使用したりできます。このセクションの周囲にロックを置くことで、コードの正確性に影響する可能性のある他のスレッドの変更を排除できます。
表 4-1 に、プログラマが一般的に使用するロックの一部を示します。OS X と iOS は、これらのロック型の大半は実装していますが、すべてではありません。サポートされていないロック型の場合、説明の列には、それらのロックがプラットフォーム上で直接実装されていない理由を説明しています。
表 4-1 ロックの型
ロック(Lock) | 説明 |
Mutex | 相互に排他的な (または mutex) ロックは、リソースの周りの保護バリアとして機能します。mutex は、一度に 1 つのスレッドのみにアクセス権を与える信号の一種です。mutex が使用中で、別のスレッドがそれを取得しようとすると、そのスレッドは元の所有者が mutex を解放するまでブロックします。複数のスレッドが同じ mutex を競合する場合、一度に 1 つのスレッドだけがそれにアクセスできます。 |
再帰的 ロック | 再帰ロックは、mutex ロックの変種です。 再帰的ロックは、1 つのスレッドが、解放する前にロックを複数回取得できるようにします。他のスレッドは、ロックの所有者が、獲得したのと同じ回数だけロックを解除するまでブロックされたままです。再帰的ロックは主に再帰的反復の間に使用されますが、複数のメソッドそれぞれが個別にロックを取得する必要がある場合にも使用できます。 |
読み書き ロック | 読み書きロックは、共有排他ロックとも呼ばれます。この型のロックは、通常、大規模操作で使用され、保護されたデータ構造が頻繁に読み込まれ、ときどきだけ変更される場合、パフォーマンスを大幅に向上させる可能性があります。通常の操作の間に、複数のリーダー (reader) が同時にデータ構造体にアクセスすることができます。しかし、スレッドが構造体に書き込みたい場合は、すべてのリーダーがロックを解放するまでブロックされ、ロックを獲得して構造体を更新することができます。書き込みスレッドがロックを待機している間、新しいリーダースレッドは書き込みスレッドが終了するまでブロックします。システムは、POSIX スレッドのみを使用する読み書きロックをサポートしています。これらのロックを使用する方法の詳細については、pthread のマニュアルページを参照してください。 |
分散 ロック | 分散ロックは、プロセスレベルで相互に排他的なアクセスを提供します。真の mutexとは異なり、分散ロックはプロセスをブロックしたり、プロセスの実行を妨げたりしません。ロックがビジー状態にあることを通知し、処理の進行方法を決定させます。 |
スピン ロック | スピンロックは、その条件が真になるまで繰り返しロック状態を調査します。スピンロックは、ロックの予想待機時間が短いマルチプロセッサシステムで最もよく使用されます。このような状況では、コンテキストスイッチとスレッドデータ構造体の更新を含む、スレッドをブロックするよりも調査するほうがより効率的です。調査の性質上、スピンロックをシステムは実装していませんが、特定の状況で簡単に実装できます。カーネルでスピンロックを実装する方法については、カーネルプログラミングガイド を参照してください。 |
ダブル チェックのロック | ダブルチェックのロックは、ロックを取る前にロック基準をテストすることによってロックを取るオーバーヘッドを減らそうとする試みです。ダブルチェックのロックは安全でない可能性があるため、システムは明示的なサポートを提供しておらず、その使用はお勧めしません。 |
ロックの使用方法についての詳細は、ロック (Locks) の使用 を参照してください。
条件
条件とは、ある条件が真であるときにスレッドが互いに信号を送ることを可能にする別の型の信号です。条件は、通常、リソースの利用可能性を示すために、またはタスクが特定の順序で実行されるようにするために使用されます。スレッドが条件をテストすると、その条件が既に真でない限りブロックされます。他の何かのスレッドが明示的に変更して条件を通知するまでブロックされたままです。条件と mutex ロックの違いは、複数のスレッドが同時に条件へのアクセスを許可される可能性があることです。この条件は、指定された基準に応じて異なるスレッドをゲートに通すゲートキーパー (門番) の機能です。
条件を使用する方法の 1 つは、保留中のイベントのプールを管理することです。イベント・キューは、キューにイベントがあったときに待機スレッドに通知するために条件変数を使用します。1 つのイベントが到着すると、キューは条件を適切に通知します。スレッドがすでに待機していた場合、スレッドは覚醒してイベントをキューから取り出して処理します。2 つのイベントがほぼ同時にキューに入った場合、キューは 2 つのスレッドを起動するために条件を 2 回通知します。
システムは、いくつかの異なる技術で条件をサポートしています。ただし、条件の正しい実装には慎重なコーディングが必要です。したがって、コードでそれを使用する前に、条件の使用 の例を参照する必要があります。
セレクタルーチンの実行
Cocoa アプリケーションには、メッセージを同期して単一のスレッドに配信する便利な方法があります。NSObject クラスは、アプリケーションのアクティブなスレッドの 1 つでセレクタを実行するためのメソッドを宣言します。これらのメソッドは、スレッドがターゲットスレッドによって同期して実行されることを保証して、スレッドが非同期的ににメッセージを配信できるようにします。たとえば、実行セレクタメッセージを使用して、分散した計算からの結果をアプリケーションのメインスレッドまたは指定された調整スレッド配布することができます。セレクタを実行する各要求は、ターゲットスレッドの実行ループの待ち行列に並ばされ、要求は受信された順序で順次処理されます。
実行セレクタルーチンの概要とそれらの使用方法の詳細については、セレクターソースを Cocoa で実行 を参照してください。
同期コストとパフォーマンス
同期は、コードの正確性を保証するのに役立ちますが、パフォーマンスを犠牲にして行います。同期ツールを使用すると、競合していない場合でも遅延が発生します。ロックとアトミック操作は、通常、メモリバリアとカーネルレベルの同期を使用して、コードが適切に保護されていることを保証します。また、ロックの競合が発生すると、スレッドがブロックされ、さらに大きな遅延が発生する可能性があります。
表 4-2 に、競合していない場合での mutex およびアトミック操作に関連するおおよそのコストを示します。これらの測定値は、数千サンプルを超える平均時間を表しています。スレッドの作成時間と同様に、mutex の取得時間 (競合していない場合でも) は、プロセッサの負荷、コンピュータの速度、および使用可能なシステムとプログラムメモリの量によって大きく異なる可能性があります。
表 4-2 Mutex とアトミック操作のコスト
項目 | おおよそのコスト | ノート |
Mutex の 取得時間 | おおよそ 0.2 マイクロ秒 | これは、競合していない場合のロック取得時間です。ロックが別のスレッドによって保持されている場合、取得時間ははるかに長くなります。この数値は、Intel の iMac で 2 GHz の Core Duo プロセッサと 1 GB の RAM (OS X v10.5) を使用した mutex 取得時に生成された平均値と中央値を分析して求めました。 |
アトミックの 比較と交換 | おおよそ 0.05 マイクロ秒 | これは、競合していない場合の比較と交換の時間です。この数値は操作の平均値と中央値を分析して決定し、Intel の iMac で 2 GHz のCore Duo プロセッサと 1 GB の RAM を搭載した OS X v10.5 で生成されました。 |
並行タスクを設計するときは、常に正確さが最も重要な要素ですが、パフォーマンス要素も考慮する必要があります。複数のスレッドでコードは正しく実行されますが、単一のスレッドで実行される同じコードよりも遅いコードはほとんど改善されません。
既存の単一スレッドアプリケーションを改造する場合は、常に重要なタスクのパフォーマンスのベースラインの測定値を取得する必要があります。追加のスレッドを追加すると、同じタスクに対して新しい測定値を取得し、マルチスレッドの場合のパフォーマンスと単一スレッドの場合のパフォーマンスを比較する必要があります。あなたのコードをチューニングした後、スレッド化がパフォーマンスを向上させない場合は、特定の実装やスレッドの使用を再考することが必要です。
パフォーマンスと計測の収集ツールについては、パフォーマンスの概要 を参照してください。 ロックおよびアトミック操作のコストの詳細については、スレッドのコスト を参照してください。
スレッドの安全性と信号
スレッド化されたアプリケーションの場合、信号を処理するという問題よりも、恐怖や混乱の原因となるものはありません。信号は、処理に情報を提供したり、何らかの方法で操作したりするために使用できる低レベルの BSD メカニズムです。いくつかのプログラムは、子プロセスの死などの特定のイベントを検出するために信号を使用します。システムは信号を使用して暴走プロセスを終了し、他の型の情報を伝達します。
信号の問題は、信号の問題ではなく、アプリケーションに複数のスレッドがある場合の動作です。単一スレッドのアプリケーションでは、すべての信号処理がメインスレッド上で実行されます。マルチスレッドアプリケーションでは、特定のハードウェアエラー (不正命令など) に結びついていない信号は、その時点で実行中のスレッドに配信されます。複数のスレッドが同時に実行されている場合は、システムが選択したスレッドに信号が配信されます。つまり、アプリケーションの任意のスレッドに信号を配信することができます。
アプリケーションで信号処理を実装するための最初のルールは、どのスレッドが信号を処理しているかについての仮定を避けることです。特定のスレッドが特定の信号を処理したい場合は、信号が到着したときにそのスレッドに通知する何らかの方法を工夫する必要があります。そのスレッドから信号処理をインストールすると、信号が同じスレッドに配信されると仮定する事は出来ません。
信号と信号処理のインストールの詳細については、signal と sigaction のマニュアルページを参照してください。
スレッドセーフな設計のヒント
同期ツールはコードをスレッドセーフにするのに便利な方法ですが、万能薬ではありません。あまりにも多くのロックやその他の型の同期プリミティブを使用すると、アプリケーションのスレッド化されたパフォーマンスは、スレッド化されていないパフォーマンスに比べて実際に低下します。安全とパフォーマンスの適切なバランスを見つけることは、経験を必要とする芸術です。以下のセクションでは、アプリケーションの適切な同期レベルを選択するためのヒントを提供します。
同期を一切避ける
新しいプロジェクトで作業する場合でも、既存のプロジェクトでも、同期の必要性を避けるためにコードとデータ構造を設計することが最良の解決策です。ロックやその他の同期ツールは便利ですが、アプリケーションのパフォーマンスに影響を与えます。全体的な設計によって特定のリソース間で強い競合が発生すると、スレッドはさらに長く待つことになります。
並行処理を実装する最善の方法は、並行タスク間の相互作用および相互依存関係を減らすことです。各タスクが独自のプライベートデータセットで操作する場合、ロックを使用してそのデータを保護する必要はありません。2 つのタスクが共通のデータセットを共有する状況であっても、そのパーティションを分割する方法や、各タスクに独自のコピーを提供する方法を検討することができます。もちろん、データセットのコピーにはコストもかかり、そのため、決定を下す前に、そのコストを同期コストと比較しなければなりません。
同期の限界を理解
同期ツールは、アプリケーション内のすべてのスレッドを一貫して使用する場合にのみ有効です。特定のリソースへのアクセスを制限するために mutex を作成する場合、リソースを操作する前に、すべてのスレッドが同じ mutex を取得しなければなりません。そうしないと、mutex が提供する保護が無効になり、プログラマーのエラーになります。
コードの正しさへの脅威に気づく
ロックとメモリバリアを使用する場合は、コード内の配置に常に注意する必要があります。うまく配置されているようなロックでさえ、実際にあなたを誤った安心感に陥らせる可能性があります。以下の一連の例は、一見無害なコードの欠陥を指摘することによって、この問題を説明しています。基本的な前提は、変更不可能なオブジェクトのセットを含む変更可能な配列があることです。配列内の最初のオブジェクトのメソッドを呼び出すとします。以下のコードを使用してそうすることとそます:
NSLock* arrayLock = GetArrayLock(); NSMutableArray* myArray = GetSharedArray(); id anObject; [arrayLock lock]; anObject = [myArray objectAtIndex:0]; [arrayLock unlock]; [anObject doSomething];
配列が変更可能であるため、配列の周りのロックは、目的のオブジェクトを取得するまで、他のスレッドが配列を変更するのを防ぎます。また、取得するオブジェクト自体が変更不可能なので、doSomething メソッドへの呼び出しの前後でロックは必要ありません。
しかし、上記の例には問題があります。doSomething メソッドを実行する前に、ロックを解除して別のスレッドが入ってきて、配列からすべてのオブジェクトを削除するとどうなるでしょう?ガベージコレクションのないアプリケーションでは、コードが保持しているオブジェクトが解放され、anObject は無効なメモリアドレスを指しているままになります。この問題を解決するには、以下のように、既存のコードを再配置し、doSomething の呼び出し後にロックを解除することができます。
NSLock* arrayLock = GetArrayLock(); NSMutableArray* myArray = GetSharedArray(); id anObject; [arrayLock lock]; anObject = [myArray objectAtIndex:0]; [anObject doSomething]; [arrayLock unlock];
ロック内に doSomething 呼び出しを移動することにより、コードは、メソッドが呼び出されたときにオブジェクトがまだ有効であることを保証します。残念ながら、doSomething メソッドの実行に時間がかかると、コードはロックを長時間保持する可能性があり、パフォーマンスのボトルネックが発生する可能性があります。
このコードの問題点は、危険な領域の定義が十分ではなく、実際の問題が理解できていないことではありません。実際の問題は、他のスレッドの存在によってのみ引き起こされるメモリ管理の問題です。別のスレッドによって解放される可能性があるため、より良い解決策は、ロックを解放する前に anObject を保持することです。この解決は、開放されようとするオブジェクトの実際の問題に対処し、潜在的なパフォーマンス上のペナルティを招くことなくこれを行います。
NSLock* arrayLock = GetArrayLock(); NSMutableArray* myArray = GetSharedArray(); id anObject; [arrayLock lock]; anObject = [myArray objectAtIndex:0]; [anObject retain]; [arrayLock unlock]; [anObject doSomething]; [anObject release];
以上の例は本質的に非常に単純ですが、非常に重要な点を示しています。正しさに関しては、明白な問題を考える必要があります。メモリ管理や設計の他の側面は、複数のスレッドが存在することによっても影響を受ける可能性があるため、これらの問題について前もって考えなければなりません。さらに、コンパイラは安全性に関して最悪の可能性を行うと常に仮定しなければなりません。この種の認識と警戒は、潜在的な問題を回避し、コードが正しく動作するようにするのに役立ちます。
プログラムをスレッドセーフにする方法の追加例については、スレッドの安全性の要約 を参照してください。
デッドロックとライブロックを監視
スレッドが同時に複数のロックを取得しようとするたびに、デッドロックが発生する可能性があります。デッドロックは、2 つの異なるスレッドが別のスレッドの必要とするロックを保持し、次に他のスレッドが保持するロックを取得しようとすると発生します。その結果、各スレッドは、他のロックを決して獲得できないため、永続的にブロックします。
ライブロックはデッドロックに似ており、2 つのスレッドが同じリソースセットで競合する場合に発生します。ライブロックの状況では、スレッドは 2 番目のロックを取得しようとして最初のロックを放棄します。2 番目のロックを取得すると、それは戻って最初のロックを再度取得しようとします。これは、1 つのロックを解除して時間を浪費し、実際の作業を行うのではなく、もう一方のロックを取得しようとします。
デッドロックとライブロックの両方の状況を回避する最良の方法は、一度に 1 つのロックのみを取得することです。一度に複数のロックを取得しなければならない場合は、他のスレッドが似たようなことをしないようにする必要があります。
揮発性変数の正しい使用
既にコードセクションを保護するために mutex を使用している場合、そのセクション内の重要な変数を保護するために volatile キーワードを使用する必要があると自動的には仮定しないでください。mutex は、ロードおよび保管操作の適切な順序付けを保証するメモリバリアを含みます。重要なセクション内の変数に volatile キーワードを追加すると、その変数にアクセスするたびに値を強制的にメモリからロードされます。特定の場合では、2 つの同期技術の組み合わせが必要な場合がありますが、パフォーマンスに大きなペナルティも生じます。変数の保護に mutex だけで十分な場合は、volatile< キーワードを省略してください。
mutex の使用を避けるために揮発性 (volatile) 変数を使用しないことも重要です。一般に、mutex やその他の同期メカニズムは、揮発性変数よりもデータ構造体の整合性を保護する優れた方法です。volatile キーワードは、変数がレジスタに格納されるのではなくメモリからロードされることだけを保証します。これは、変数がコードによって正しくアクセスされることを保証するものではありません。
アトミック (微少) 操作の使用
ブロックしにで同期するのは、いくつかの型の操作を実行し、ロックの費用を回避する方法です。ロックは 2 つのスレッドを同期させる有効な方法ですが、ロックを取得することは、競合していない場合でも比較的高価な操作です。これとは対照的に、多くのアトミック操作では、完了に時間がかかりますが、ロックと同じくらい効果的です。
アトミック操作を使用すると、32 ビットまたは 64 ビットの値に対して簡単な数学的および論理的操作を実行できます。これらの操作は、特別なハードウェア命令 (およびオプションのメモリバリア) に依存して、影響を受けるメモリが再びアクセスされる前に所定の操作が完了する事を保証します。マルチスレッドの場合、スレッド間の同期が正しく行われるように、メモリーバリアを組み込んだアトミック操作を常に使用する必要があります。
表 4-3 に、使用可能なアトミックの数学演算と論理演算、および対応する関数名を示します。これらの関数はすべて /usr/include/libkern/OSAtomic.h ヘッダーファイルで宣言されており、ここで完全な構文も見つかります。64 ビットバージョンのこれらの関数は、64 ビットプロセッサでのみ使用できます。
表 4-3 アトミックな数学演算と論理演算
演算 | 関数名 | 説明 |
加算 | OSAtomicAdd32 OSAtomicAdd32Barrier OSAtomicAdd64 OSAtomicAdd64Barrier | 2 つの整数値を加算し、指定された変数の 1 つに結果を格納します。 |
増分 | OSAtomicIncrement32 OSAtomicIncrement32Barrier OSAtomicIncrement64 OSAtomicIncrement64Barrier | 指定された整数値を 1 だけ増分します。 |
減分 | OSAtomicDecrement32 OSAtomicDecrement32Barrier OSAtomicDecrement64 OSAtomicDecrement64Barrier | 指定された整数値を 1 だけ減分します。 |
論理 OR | OSAtomicOr32 OSAtomicOr32Barrier | 指定された 32 ビット値と 32 ビットマスク間の論理 OR を実行します。 |
論理 AND | OSAtomicAnd32 OSAtomicAnd32Barrier | 指定された 32 ビット値と 32 ビットマスク間の論理 AND を実行します。 |
論理 XOR | OSAtomicXor32 OSAtomicXor32Barrier | 指定された 32 ビット値と 32 ビットマスク間の論理 XOR を実行します。 |
比較し 交換 | OSAtomicCompareAndSwap32 OSAtomicCompareAndSwap32Barrier OSAtomicCompareAndSwap64 OSAtomicCompareAndSwap64Barrier OSAtomicCompareAndSwapPtr OSAtomicCompareAndSwapPtrBarrier OSAtomicCompareAndSwapInt OSAtomicCompareAndSwapIntBarrier OSAtomicCompareAndSwapLong OSAtomicCompareAndSwapLongBarrier | 変数を指定された古い値と比較します。2 つの値が等しい場合、この関数は指定された新しい値を変数に代入します。それ以外の場合は、何もしません。比較と代入は 1 つのアトミック操作として実行され、関数は交換 (swap) が実際に発生したかどうかを示すブール値を返します。 |
テストと設定 | OSAtomicTestAndSet OSAtomicTestAndSetBarrier | 指定された変数のビットをテストし、そのビットを 1 に設定し、古いビットの値をブール値として返します。ビットは、バイト ((char*)address +(n >> 3)) の式 (0x80 >>(n&7)) に従ってテストされ、ここで n はビット数、address は変数へのポインタです。この式は、変数を効果的に 8 ビットサイズの塊に分割し、各塊のビットを逆順に並べ替えます。たとえば、32 ビット整数の最下位ビット (ビット 0) をテストするには、実際にビット数に 7 を指定します。同様に、最上位ビット (ビット 32) をテストするには、ビット数に 24 を指定します。 |
テストとクリア | OSAtomicTestAndClear OSAtomicTestAndClearBarrier | 指定された変数のビットをテストし、そのビットを 0 に設定し、古いビットの値をブール値として返します。ビットは、バイト ((char*)address +(n >> 3)) の式 (0x80 >>(n&7)) に従ってテストされ、ここで n はビット数、address は変数へのポインタです。この式は、変数を効果的に 8 ビットサイズの塊に分割し、各塊のビットを逆順に並べ替えます。たとえば、32 ビット整数の最下位ビット (ビット 0) をテストするには、実際にビット数に 7 を指定します。同様に、最上位ビット (ビット 32) をテストするには、ビット数に 24 を指定します。 |
大部分のアトミック関数の動作は、比較的簡単で、あなたが期待するものでなければなりません。しかし、リスト 4-1 は、アトミックなテストと設定および比較し交換の動作を示し、これらはやや複雑です。OSAtomicTestAndSet 関数への最初の 3 つの呼び出しは、整数値に使用されているビット操作式とその結果が期待どおりとは異なることを示しています。最後の 2 つの呼び出しは、OSAtomicCompareAndSwap32 関数の動作を示しています。すべての場合で、これらの関数は、他のスレッドが値を操作していない場合に、競合のない場合に呼び出されます。
リスト 4-1 アトミック操作の実行
int32_t theValue = 0; OSAtomicTestAndSet(0, &theValue); // theValue is now 128. theValue = 0; OSAtomicTestAndSet(7, &theValue); // theValue is now 1. theValue = 0; OSAtomicTestAndSet(15, &theValue) // theValue is now 256. OSAtomicCompareAndSwap32(256, 512, &theValue); // theValue is now 512. OSAtomicCompareAndSwap32(256, 1024, &theValue); // theValue is still 512.
アトミック操作の詳細については、atomic マニュアルページおよび /usr/include/libkern/OSAtomic.h ヘッダーファイルを参照してください。
ロック (Locks) の使用
ロックは、スレッドプログラミングの基本的な同期ツールです。ロックを使用すると、コードの大部分を簡単に保護して、コードの正確性を保証することができます。OS X と iOS はすべてのアプリケーション型に対して基本的な mutex ロックを提供し、Foundation フレームワークは特別な状況のために mutex ロックのいくつかの変形を定義しています。以下のセクションでは、これらのロック型のいくつかを使用する方法を示します。
POSIX mutex ロックの使用
POSIX mutex ロックは、どのアプリケーションからでも非常に使いやすいです。mutex ロックを作成するには、 pthread_mutex_t 構造体を宣言して初期化します。mutex ロックをロックしたりロック解除したりするには、 pthread_mutex_lock および pthread_mutex_unlock 関数を使用して下さい。リスト 4-2 は、POSIX スレッドの mutex ロックの初期化と使用に必要な基本コードを示しています。ロックが終了したら、 pthread_mutex_destroy を呼び出してロックデータ構造体を解放します。
リスト 4-2 mutex ロックの使用
pthread_mutex_t mutex; void MyInitFunction() { pthread_mutex_init(&mutex, NULL); } void MyLockingFunction() { pthread_mutex_lock(&mutex); // Do work. pthread_mutex_unlock(&mutex); }
NSLock クラスの使用
NSLock オブジェクトは、Cocoa アプリケーション用の基本の Mutex を実装します。すべてのロック (NSLock を含む) のインターフェースは、lockと unlock のメソッドを定義する NSLocking プロトコルによって実際に定義されます。mutex と同じように、これらのメソッドを使用してロックを取得および解放します。
標準のロック動作に加えて、NSLock クラスは tryLock メソッドと lockBeforeDate: メソッドを追加します。tryLock メソッドはロックを取得しようとしますが、ロックが利用できない場合はブロックしません。代わりに、このメソッドは単純に NO を返します。lockBeforeDate: メソッドは、ロックを取得しようとしますが、指定された制限時間内にロックが取得されない場合、スレッドのロックを解除します (NO を返します)。
以下の例は、NSLock オブジェクトを使用して、データが複数のスレッドによって計算されているディスプレイの更新を調整する方法を示しています。スレッドがロックをすぐに取得できない場合は、スレッドがロックを取得してディスプレイを更新するまで、スレッドは計算を続行します。
BOOL moreToDo = YES; NSLock *theLock = [[NSLock alloc] init]; ... while (moreToDo) { /* Do another increment of calculation */ /* until there’s no more to do. */ if ([theLock tryLock]) { /* Update display used by all threads. */ [theLock unlock]; } }
@synchronized 指令の使用
@synchronized 指令は、Objective-C コードで即座に mutex ロックを作成する便利な方法です。@synchronized 指令は、他の mutex ロックが行なうことを行ないます。異なるスレッドが同時に同じロックを取得するのを妨げます。ただし、この場合は、mutex またはロックオブジェクトを直接作成する必要はありません。代わりに、Objective-C オブジェクトをロック・トークンとして使用するだけです (以下の例を参照)。
- (void)myMethod:(id)anObj { @synchronized(anObj) { // Everything between the braces is protected by the @synchronized directive. } }
@synchronized 指令に渡されたオブジェクトは、保護されたブロックを区別するために使用される一意の ID です。前述のメソッドを 2 つの異なるスレッドで実行し、各スレッドの anObj パラメータに異なるオブジェクトを渡すと、各スレッドはロックを取り、別のスレッドによってブロックされずに処理を続行します。ただし、どちらの場合も同じオブジェクトを渡すと、スレッドの 1 つが最初にロックを取得し、もう 1 つが最初のスレッドが重要なセクションを完了するまでブロックします。
予防策として、@synchronized ブロックは暗黙的に保護されたコードに例外ハンドラを追加します。このハンドラは、例外が throw された場合に自動的に mutex を解放します。つまり、@synchronized 指令を使用するには、コード内で Objective-C の例外処理も有効にしなければなりません。暗黙的な例外ハンドラによるオーバーヘッドを追加したくない場合は、ロッククラスの使用を検討する必要があります。
@synchronized 指令の詳細については、Objective-C プログラミング言語 を参照してください。
他の Cocoa のロックの使用
以下のセクションでは、他のいくつかの型の Cocoa ロックを使用するプロセスについて説明します。
NSRecursiveLock オブジェクトの使用
NSRecursiveLock クラスは、スレッドをデッドロックにさせずに同じスレッドによって複数回取得できるロックを定義します。再帰的なロック (recursive lock) は、取得の成功した回数を追跡します。ロックの成功した取得のそれぞれは、対応するロックのロック解除の呼び出しによってバランスを保たなければなりません。すべてのロックとロック解除の呼び出しのバランスが取れている場合にのみ、ロックは実際に解放され、他のスレッドがロックを取得できるようになります。
その名前が暗示するように、この型のロックは再帰関数がスレッドをブロックするのを防ぐために再帰関数内で一般的に使用されます。同様に、再帰的でない場合にもそれを使用して、セマンティクスがロックを取ることも要求する関数を呼び出すことができます。再帰を介してロックを取得する簡単な再帰関数の例を以下に示します。このコードで NSRecursiveLock オブジェクトを使用しなかった場合、その関数は再び呼び出されたときにスレッドがデッドロックに陥ります。
NSRecursiveLock *theLock = [[NSRecursiveLock alloc] init]; void MyRecursiveFunction(int value) { [theLock lock]; if (value != 0) { --value; MyRecursiveFunction(value); } [theLock unlock]; } MyRecursiveFunction(5);
NSConditionLock オブジェクトの使用
NSConditionLock オブジェクトは、特定の値でロックおよびロック解除できる mutex ロックを定義します。この型のロックを条件と混同しないでください (条件 を参照)。この動作は、条件と多少似ていますが、実装は非常に異なります。
通常、あるスレッドが別のスレッドが消費するデータを生成する場合など、スレッドが特定の順序でタスクを実行する必要がある場合は、NSConditionLock オブジェクトを使用して下さい。データを生成するスレッドの実行中、消費側はプログラムに特有の条件を使用してロックを取得します。(条件自体はあなたが定義した整数値です。) 生成側が終了すると、ロックを解除し、ロック条件を適切な整数値に設定して消費側のスレッドを起動し、データを処理します。
NSConditionLock オブジェクトが応答するロックおよびロック解除のメソッドは、任意の組み合わせで使用できます。たとえば、lock メッセージを unlockWithCondition: と組み合わせたり、lockWhenCondition: メッセージを unlock と組み合わせることができます。もちろん、この後者の組み合わせはロックを解除しますが、特定の条件値で待機しているスレッドを解放しない可能性があります。
以下の例は、条件ロックを使用して生成側/消費側の問題を処理する方法を示しています。アプリケーションにデータのキューが含まれているとします。生成側のスレッドはキューにデータを追加し、消費側のスレッドはキューからデータを抽出します。生成側は特定の条件を待つ必要はありませんが、データをキューに安全に追加できるようにロックが使用可能になるまで待機しなければなりません。
id condLock = [[NSConditionLock alloc] initWithCondition:NO_DATA]; while(true) { [condLock lock]; /* Add data to the queue. */ [condLock unlockWithCondition:HAS_DATA]; }
ロックの初期条件は NO_DATA に設定されているため、生成側のスレッドは最初にロックを取得する際に問題がありません。これはキューをデータで満たし、条件を HAS_DATA に設定します。その後の繰り返しでは、生成側のスレッドは、キューが空であるか、まだ何らかのデータがあるかどうかにかかわらず、到着時に新しいデータを追加できます。ブロックするのは、消費側のスレッドがキューからデータを抽出しているときだけです。
消費側のスレッドは処理すべきデータを持たなければならないため、特定の条件を使用してキューを待機します。生成側がキューにデータを置くと、消費側のスレッドは覚醒してロックを取得します。次に、キューからデータを抽出し、キューのステータスを更新することができます。以下の例は、消費側のスレッドの処理ループの基本的な構造を示しています。
while (true) { [condLock lockWhenCondition:HAS_DATA]; /* Remove data from the queue. */ [condLock unlockWithCondition:(isEmpty ? NO_DATA : HAS_DATA)]; // Process the data locally. }
NSDistributedLock オブジェクトの使用
NSDistributedLock クラスは、ファイルなどの共有リソースへのアクセスを制限するために、複数のホスト上の複数のアプリケーションで使用できます。ロック自体は事実上、ファイルやディレクトリなどのファイルシステム項目を使用して実装される mutex ロックです。NSDistributedLock オブジェクトを使用可能にするには、そのロックを使用するすべてのアプリケーションが書き込み可能でなければなりません。これは通常、アプリケーションを実行しているすべてのコンピュータにアクセス可能なファイルシステムにそれを置くことを意味します。
他の型のロックとは異なり、NSDistributedLock は NSLocking プロトコルに準拠していないため、lock メソッドはありません。lock メソッドは、スレッドの実行をブロックし、システムに所定の頻度でロックをポーリングするよう要求します。コードにこのペナルティを課すのではなく、NSDistributedLock は tryLock メソッドを提供し、ポーリングするかどうかを決めることができます。
ファイルシステムを使用して実装されているため、NSDistributedLock オブジェクトは、所有者が明示的に解放しない限り解放されません。分散ロックを保持しているときにアプリケーションがクラッシュした場合、他のクライアントは保護されたリソースにアクセスできなくなります。このような状況では、breakLock メソッドを使用して既存のロックを解除し、取得できるようにすることができます。ただし、所有プロセスが終了してロックを解除できない場合を除き、一般に、ロックを解除する必要はありません。
他の型のロックと同様に、NSDistributedLock オブジェクトの使用が終了したら、unlock メソッドを呼び出して解放してください。
条件の使用
条件は、操作を進めるべき順序を同期させるために使用できる特別な型のロックです。mutex ロックとは微妙に異なります。条件を待っているスレッドは、その条件が別のスレッドによって明示的に通知されるまでブロックされたままです。
オペレーティングシステムの実装に伴う微妙な操作のため、条件ロックはコードによって実際には通知されなかったとしても、偽の成功で戻ってくることが許されます。これらの偽信号によって引き起こされる問題を回避するには、常に条件ロックと一緒に述語 (predicate) を使用する必要があります。述語はスレッドが進行するのが安全かどうかを判断する、より具体的な方法です。条件は、述語が信号スレッドによって設定されるまで、単にスレッドをスリープ状態に保ちます。
以下のセクションでは、コードで条件を使用する方法を示します。
NSCondition クラスの使用
NSCondition クラスは、POSIX 条件と同じセマンティクスを提供しますが、必要なロックおよび条件データ構造体の両方を 1 つのオブジェクトに包み込みます。結果は、あなたが mutex のようにロックして、条件のように待つことができるオブジェクトです。
リスト 4-3 は、NSCondition オブジェクトを待機するイベントのシーケンスを示すコードスニペットを示しています。cocoaCondition 変数には NSCondition オブジェクトが含まれ、timeToDoWork 変数は、条件を通知する直前に別のスレッドから増分される整数です。
リスト 4-3 Cocoa の条件の使用
[cocoaCondition lock]; while (timeToDoWork <= 0) [cocoaCondition wait]; timeToDoWork--; // Do real work here. [cocoaCondition unlock];
リスト 4-4 は、Cocoa の状態を通知し、predicate 変数を増分するために使用されるコードを示しています。条件を通知する前に常に条件をロックする必要があります。
リスト 4-4 Cocoa 条件の通知
[cocoaCondition lock]; timeToDoWork++; [cocoaCondition signal]; [cocoaCondition unlock];
POSIX 条件の使用
POSIX スレッド条件ロックでは、条件データ構造体と mutex の両方を使用する必要があります。2 つのロック構造は別々ですが、mutex ロックは実行時に条件構造体に密接に関連しています。信号待ちのスレッドは、常に同じ mutex ロックと条件構造体を一緒に使用する必要があります。ペアリングを変更するとエラーが発生する可能性があります。
リスト 4-5 は、条件と述語の基本的な初期化と使用法を示しています。条件と mutex ロックの両方を初期化した後、待ちスレッドは ready_to_go 変数をその述語として使用して while ループに入ります。述語が設定され、続いて条件が通知された場合にのみ、待機スレッドは覚醒して作業を開始します。
リスト 4-5 POSIX 条件の使用
pthread_mutex_t mutex; pthread_cond_t condition; Boolean ready_to_go = true; void MyCondInitFunction() { pthread_mutex_init(&mutex); pthread_cond_init(&condition, NULL); } void MyWaitOnConditionFunction() { // Lock the mutex. pthread_mutex_lock(&mutex); // If the predicate is already set, then the while loop is bypassed; // otherwise, the thread sleeps until the predicate is set. while(ready_to_go == false) { pthread_cond_wait(&condition, &mutex); } // Do work. (The mutex should stay locked.) // Reset the predicate and release the mutex. ready_to_go = false; pthread_mutex_unlock(&mutex); }
信号 (signaling) スレッドは、述語を設定し、信号を条件ロックに送信する両方の役割を担います。リスト 4-6 に、この動作を実装するためのコードを示します。この例では、状態を待っているスレッド間で競合状態が発生しないように、条件が mutex 内部で通知されます。
リスト 4-6 条件ロックの通知
void SignalThreadUsingCondition() { // At this point, there should be work for the other thread to do. pthread_mutex_lock(&mutex); ready_to_go = true; // Signal the other thread to begin work. pthread_cond_signal(&condition); pthread_mutex_unlock(&mutex); }