実行ループ
実行ループは、スレッドに関連する基本的なインフラストラクチャの一部です。実行ループ とは、作業をスケジュール化し、着信イベントの受信を調整するために使用するイベント処理ループです。実行ループの目的は、実行する作業があるときスレッドをビジー状態に保ち、スレッドが存在しないときにスレッドをスリープ状態にすることです。
実行ループの管理は完全に自動ではありません。適切な時間に実行ループを開始し、着信イベントに応答するためにスレッドのコードを設計しなければなりません。Cocoa と Core Foundation は、実行ループオブジェクト を提供し、スレッドの実行ループを構成し、また管理するのに役立ちます。アプリケーションでこれらのオブジェクトを明示的に作成する必要はありません。アプリケーションのメインスレッドを含む各スレッドには、関連する実行ループオブジェクトがあります。ただし、二次スレッドだけが明示的に実行ループを実行する必要があります。アプリのフレームワークは、アプリケーション起動プロセスの一環としてメインスレッド上で実行ループを自動的にセットアップして実行します。
以下のセクションでは、実行ループと、アプリケーションのためにそれらを構成する方法について詳しく説明します。実行ループオブジェクトの追加の詳細については、NSRunLoop クラスリファレンス および CFRunLoop リファレンス を参照してください。
実行ループの解剖学
実行ループは、その名前の響きに非常によく似ています。これは、スレッドが入ってくるループで、着信イベントに応じてイベントハンドラを実行します。あなたのコードでは、実行ループの実際のループ部分を実装するために使用される制御文を提供します。つまり、コードでは実行ループを駆動する while ループまたは for ループを提供します。ループ内では、実行ループオブジェクトを使用して、イベントを受け取り、インストールされているハンドラを呼び出すイベント処理コードを "実行" します。
実行ループは、2 つの異なる型のソースからイベントを受信します。入力ソース は、非同期イベントを配信し、通常は別のスレッドまたは別のアプリケーションからメッセージを配信します。タイマーソース は、スケジュールされた時間または繰り返し間隔で発生する同期イベントを配信します。どちらの型のソースも、アプリケーション特有のハンドラ・ルーチンを使用して、到着時にイベントを処理します。
図 3-1 に、実行ループやさまざまなソースの概念的構造を示します。入力ソースは、非同期イベントを対応するハンドラに配信し、runUntilDate: メソッド (スレッドに関連した NSRunLoop オブジェクトで呼び出されます) を終了させます。タイマーソースは、ハンドラルーチンにイベントを配信しますが、実行ループを終了させません。
図 3-1 実行ループとそのソースの構造
実行ループは、入力されたソースを処理するだけでなく、実行ループの動作に関する通知も生成します。登録された 実行ループ監視者 は、これらの通知を受信し、それらを使用してスレッドに対して追加の処理を行うことができます。Core Foundation を使用して、実行ループ監視者をスレッドにインストールして下さい。
以下のセクションでは、実行ループの構成成分とそれらが操作するモードについて詳しく説明します。また、イベントの処理中に異なる時刻に生成される通知についても説明します。
実行ループモード
実行ループモード は、監視されるべき入力ソースとタイマーの集合であり、通知されるべき実行ループ監視者の集合です。実行ループを実行するたびに、実行するべき特定の "モード" を (明示的または暗黙的に) 指定します。実行ループのそのパスの間に、そのモードに関連するソースのみが監視され、イベントを配信することが許可されます。 (同様に、そのモードに関連した監視者だけが実行ループの進行を通知されます。) 他のモードに関連するソースは、その後、適切なモードでループを通過するまで、新しいイベントを保持します。
あなたのコードでは、名前でモードを識別します。Cocoa と Core Foundation はどちらも、デフォルトモードと一般的に使用されるいくつかのモードを定義し、コード内のそれらのモードを指定する文字列を定義しています。モード名にカスタム文字列を指定するだけで、カスタムモードを定義することができます。カスタムモードに割り当てる名前は任意ですが、これらのモードの内容は異なります。1 つ以上の入力ソース、タイマー、または実行ループ監視者を、作成するすべてのモードに役に立つように追加することが必要です。
実行ループを通過する際に、不要なソースからのイベントをフィルタリングするモードを使用します。ほとんどの場合、システム定義の "デフォルト" モードで実行ループを実行する必要があります。ただし、モーダルパネルは "モーダル" モードで実行される場合があります。このモードでは、モーダルパネルに関連するソースのみがスレッドにイベントを配信します。二次スレッドでは、カスタムモードを使用して、時間が重要な操作中に優先度の低いソースがイベントを配信しないようにすることができます。
表 3-1 に、Cocoa と Core Foundation で定義されている標準モードと、そのモードをいつ使用するかの説明を示します。"名前" の列には、コード内のモードを指定するために使用する実際の定数をリストしています。
表 3-1 定義済みの実行ループモード
モード | 名前 | 説明 |
デフォルト | NSDefaultRunLoopMode (Cocoa) kCFRunLoopDefaultMode (Core Foundation) | デフォルトモードは、ほとんどの操作で使用されるモードです。ほとんどの時間、このモードを使用して実行ループを開始し、入力ソースを構成します。 |
結合(Connection) | NSConnectionReplyMode (Cocoa) | Cocoa はこのモードを NSConnection オブジェクトと共に使用して応答を監視します。自分でこのモードを使う必要はほとんどありません。 |
モーダル(Modal) | NSModalPanelRunLoopMode (Cocoa) | Cocoa はこのモードを使用して、モーダルパネル用に意図されたイベントを識別します。 |
イベント追跡 | NSEventTrackingRunLoopMode (Cocoa) | Cocoa はこのモードを使用して、マウスドラッグループや他の種類のユーザーインターフェイス・トラッキング・ループ中に受信イベントを制限します。 |
共通モード | NSRunLoopCommonModes (Cocoa) kCFRunLoopCommonModes (Core Foundation) | これは一般に使用されるモードの構成可能なグループです。入力ソースをこのモードに関連付けると、グループ内の各モードにも関連付けます。Cocoa アプリケーションの場合、このセットにはデフォルトで、デフォルト、モーダル、イベント追跡モードが含まれています。Core Foundation には、最初はデフォルト・モードのみが含まれます。 CFRunLoopAddCommonMode 関数を使用して、カスタムモードをセットに追加できます。 |
入力ソース
入力ソースは、イベントをスレッドに非同期的に配信します。イベントのソースは、入力ソースの型に依存し、入力ソースの型は、一般的に 2 つのカテゴリのうちの 1 つです。ポートを基礎にした入力ソースは、アプリケーションの Mach ポートを監視します。カスタム入力ソースは、イベントのカスタムソースを監視します。実行ループに関する限り、入力ソースがポートを基礎にしているかカスタムであるかは関係ありません。システムは、通常、使用できる両方の型の入力ソースを実装します。2 つのソースの唯一の違いは、それらがどのように通知されるかです。ポートを基礎にしたソースはカーネルによって自動的に通知されますが、カスタムソースは別のスレッドから手動で通知しなければなりません。
入力ソースを作成するときは、実行ループの 1 つ以上のモードに割り当てます。モードは、どの瞬間にどの入力ソースが監視されるかに影響します。ほとんどの場合、デフォルトモードで実行ループを実行しますが、カスタムモードも指定できます。入力ソースが現在監視されているモードでない場合、それが生成したイベントは、実行ループが正しいモードで実行されるまで保持されます。
以下のセクションでは、いくつかの入力ソースについて説明します。
ポートを基本としたソース
Cocoa と Core Foundation は、ポート関連のオブジェクトと関数を使用してポートを基本とした入力ソースを作成するための組み込みのサポートを提供します。たとえば、Cocoa では入力ソースを直接作成する必要は全くありません。単にポートオブジェクトを作成し、NSPort のメソッドを使用してそのポートを実行ループに追加するだけです。ポートオブジェクトは、必要な入力ソースの作成と構成を処理します。
Core Foundation では、ポートとその実行ループソースの両方を手動で作成しなければなりません。いずれの場合も、ポートの不透明型 (CFMachPortRef、CFMessagePortRef、または CFSocketRef) に関連した関数を使用して、適切なオブジェクトを作成して下さい。
カスタム入力ソース
カスタム入力ソースを作成するには、Core Foundation で CFRunLoopSourceRef の不透明型に関連付した関数を使用しなければなりません。いくつかの呼び出し関数を使用してカスタム入力ソースを構成して下さい。Core Foundation は、ソースを構成し、着信イベントを処理し、実行ループから削除されたときにソースを破棄するために、異なる点でこれらの機能を呼び出します。
イベントが到着したときにカスタムソースの動作を定義することに加えて、イベント配信メカニズムも定義しなければなりません。ソースのこの部分は別のスレッド上で実行され、入力ソースにデータを提供し、そのデータの処理準備ができたらそれを通知する役割を担います。イベント配信の仕組みはあなた次第ですが、過度に複雑である必要はありません。
カスタム入力ソースを作成する方法の例については、カスタム入力ソースの定義 を参照してください。カスタム入力ソースのリファレンス情報については、CFRunLoopSource リファレンス も参照してください。
セレクターソースを Cocoa で実行
ポートを基本としたソースに加えて、Cocoa は任意のスレッドでセレクタを実行できるカスタム入力ソースを定義しています。ポートを基本としたソースと同様に、実行セレクタ要求はターゲットスレッドでシリアル化され、複数のメソッドが 1 つのスレッドで実行される際に発生する可能性がある同期問題の多くを軽減します。ポートを基本としたソースとは異なり、実行セレクタソースはセレクタを実行した後に実行ループから自身を削除します。
別のスレッドでセレクタを実行するときは、ターゲットスレッドはアクティブな実行ループを必ず持たなければなりません。作成するスレッドについては、これはコードが明示的に実行ループを開始するまで待つことを意味します。ただし、メインスレッドは独自の実行ループを開始するため、アプリケーションがアプリケーションデリゲートの applicationDidFinishLaunching: メソッドを呼び出すとすぐに、そのスレッドで呼び出しを始めることができます。実行ループは、各ループ繰り返しの間にループを処理するのではなく、ループを通過するたびに、セレクタ呼び出しを実行する全ての待ち行列を処理します。
表 3-2 に、他のスレッドでセレクタを実行するために使用できる NSObject で定義されたメソッドを示します。これらのメソッドは NSObject で宣言されているため、POSIX スレッドを含む Objective-C オブジェクトにアクセスできる全てのスレッドから使用できます。これらのメソッドは、実際にはセレクタを実行するための新しいスレッドを作成しません。
表 3-2 他のスレッドでのセレクタの実行
メソッド | 説明 |
performSelectorOnMainThread: withObject:waitUntilDone: performSelectorOnMainThread: withObject:waitUntilDone:modes: | そのスレッドの次の実行ループサイクル中に、アプリケーションのメインスレッド上で指定されたセレクタを実行します。これらのメソッドは、セレクタが実行されるまで現在のスレッドをブロックするオプションを提供します。 |
performSelector:onThread: withObject:waitUntilDone: performSelector:onThread: withObject:waitUntilDone:modes: | NSThread オブジェクトを持つ任意のスレッド上で、指定されたセレクタを実行します。これらのメソッドは、セレクタが実行されるまで現在のスレッドをブロックするオプションを提供します。 |
performSelector:withObject: afterDelay: performSelector:withObject: afterDelay:inModes: | 次の実行ループサイクル中およびオプションの遅延期間の後に、現在のスレッド上で指定されたセレクタを実行します。セレクタを実行するために次の実行ループサイクルまで待つので、これらのメソッドは現在実行中のコードから自動の少しの遅延を提供します。キューに入れられた複数のセレクタは、キューに入れられた順序で次々に実行されます。 |
cancelPreviousPerformRequestsWithTarget: cancelPreviousPerformRequestsWithTarget: selector:object: | performSelector:withObject:afterDelay: または performSelector:withObject:afterDelay:inModes: メソッドを使用して現在のスレッドに送信されたメッセージをキャンセルすることができます。 |
これらの各メソッドの詳細については、NSObject クラスリファレンス を参照してください。
タイマーソース
タイマーソースは、あらかじめ設定された将来の時間にスレッドに同期してイベントを配信します。タイマーは、スレッドが何かをするように通知する方法です。例えば、検索フィールドは、ユーザからの連続するキーストロークの間に一定の時間が経過すると、タイマーを使用して自動検索を開始することができます。この遅延時間を使用することにより、検索を開始する前に、できるだけ多くの所望の検索文字列を入力する機会がユーザに与えられます。
タイマーは時間を基本とした通知を生成しますが、タイマーはリアルタイムのメカニズムではありません。入力ソースと同様に、タイマーは実行ループの特定のモードに関連しています。タイマーが現在実行ループによって監視されているモードにない場合、タイマーのサポートされているモードの 1 つで実行ループを実行するまで、タイマーは起動しません。同様に、実行ループがハンドラルーチンの実行中のときにタイマーが起動すると、タイマーは実行ループを介して次にハンドラルーチンを呼び出すまで待機します。実行ループがまったく実行されていない場合、タイマーは決して起動しません。
イベントを 1 回だけ、または繰り返し生成するようにタイマーを構成できます。反復タイマーは、実際の起動時間ではなく、予定された起動時間に基づいて自動的に再調整されます。たとえば、タイマーが特定の時間に 5 秒間隔で起動するようにスケジュールされている場合、実際の起動時間が遅れても、スケジュールされた起動時間は常に元の 5 秒の時間間隔になります。起動時間が非常に遅れて予定された起動時間のうちの 1 つ以上を失敗すると、タイマーは失敗した時間に対して 1 回だけ起動されます。失敗した後起動した後、次の予定された起動時間のためにタイマーは再スケジュールされます。
タイマーソースの構成の詳細については、タイマーソースの構成 を参照してください。リファレンスの情報については、NSTimer クラスリファレンス またはCFRunLoopTimer リファレンス を参照してください。
実行ループ監視者
適切な非同期イベントまたは同期イベントが発生したときに起動するソースとは対照的に、実行ループ監視者は、実行ループ自体の実行中に特別な場所で起動します。実行ループ監視者を使用して、特定のイベントを処理するスレッドを準備するか、スレッドがスリープ状態になる前にスレッドを準備することができます。実行ループ監視者は、実行ループ内の以下のイベントと関連付けることができます。
- 実行ループの入り口。
- 実行ループがタイマーを処理しようとしているとき。
- 実行ループが入力ソースを処理しようとしているとき。
- 実行ループがスリープ状態になるとき。
- 実行ループが起きたが、目を覚ませるイベントを処理する前。
- 実行ループからの終了。
Core Foundation を使用してアプリに実行ループ監視者を追加することができます。実行ループ監視者を作成するには、CFRunLoopObserverRef の不透明型の新しいインスタンスを作成します。この型では、カスタムコールバック関数と関心のあるアクティビティを追跡します。
タイマーと同様に、実行ループ監視者は 1 回または繰り返し使用できます。ワンショット監視者は、起動後に実行ループから自分自身を削除し、それに対し繰り返し監視者は接続されたままです。監視者を作成するときに、監視者を 1 回実行するか、繰り返し実行するかを指定します。
実行ループ監視者を作成する方法の例については、実行ループの構成 を参照してください。 リファレンスの情報については、CFRunLoopObserver リファレンス を参照してください。
イベントの実行ループシーケンス
スレッドの実行ループは、実行するたびに、保留中のイベントを処理し、接続されている監視者の通知を生成します。これを行う順序は非常に具体的で、以下のとおりです。
- 実行ループが入力されたことを監視者に通知します。
- 準備が整ったタイマーが起動しようとしていることを監視者に通知します。
- ポートを基本としない入力ソースが起動しようとしていることを監視者に通知します。
- 起動する準備ができているポートを基本としない入力ソースをすべて起動します。
- ポートを基本とした入力ソースが準備完了状態で、待機している場合は、すぐにイベントを処理します。手順 9 に進みます。
- スレッドがスリープしようとしていることを監視者に通知します。
- 以下のいずれか 1 つのイベントが発生するまで、スレッドをスリープ状態にします。
- ポートを基本とした入力ソースのイベントが到着します。
- タイマーが起動します。
- 実行ループに設定されたタイムアウト値が期限切れになります。
- 実行ループは明示的に起動されます。
- スレッドが起きたことを監視者に通知します。
- 保留中のイベントを処理します。
- ユーザー定義のタイマーが起動した場合は、タイマーイベントを処理してループを再開します。手順 2 に進みます。
- 入力ソースが起動した場合は、イベントを配信します。
- 実行ループが明示的に起動されたが、まだタイムアウトしていない場合は、ループを再開します。手順 2 に進みます。
- 実行ループが終了したことを監視者に通知します。
実際にイベントが発生する前に、タイマーと入力ソースの監視者通知が配信されるため、通知の時刻と実際のイベントの時刻との間にはずれが存在する可能性があります。これらのイベント間のタイミングが重要な場合は、スリープおよびスリープからの覚醒通知を使用して、実際のイベント間のタイミングを関連付けることができます。
実行ループを実行するときにタイマーやその他の定期的なイベントが配信されるため、そのループを回避すると、それらのイベントの配信が中断されます。この動作の典型的な例は、ループを入力し、アプリケーションからイベントを繰り返し要求することによって、マウス追跡ルーチンを実装するたびに発生します。あなたのコードは、アプリケーションがそれらのイベントを正常に急送するのではなく、イベントを直接取得し、アクティブなタイマーがマウス追跡ルーチンが終了してアプリケーションに制御を戻すまで起動できません。
実行ループは、実行ループオブジェクトを使用して明示的に起動することができます。他のイベントによっても、実行ループが起動する可能性があります。たとえば、ポートを基本としない別の入力ソースを追加すると、実行ループが起動し、他のイベントが発生するまで待つのではなく、入力ソースをすぐに処理できるようになります。
実行ループをいつ使用すべきか?
実行ループを明示的に実行する必要があるのは、アプリケーション用の二次スレッドを作成するときだけです。アプリケーションのメインスレッドの実行ループは、インフラストラクチャの重要な部分です。その結果、アプリのフレームワークはメインのアプリケーションループを実行し、そのループを自動的に開始するためのコードを提供します。iOS (または OS X の NSApplication) での UIApplication の run メソッドは、通常の起動シーケンスの一部としてアプリケーションのメインループを開始します。Xcode テンプレートプロジェクトを使用してアプリケーションを作成する場合は、これらのルーチンを明示的に呼び出す必要は決してありません。
二次スレッドの場合は、実行ループが必要かどうかを判断する必要があります。実行ループが必要な場合は、それをあなた自身で構成して開始して下さい。すべてのケースでスレッドの実行ループを開始する必要はありません。たとえば、スレッドを使用して長時間実行され、前もって決定されたタスクを実行する場合、おそらく実行ループの開始を避けることができます。実行ループは、スレッドとのインタラクティブ性をさらに高めるためのものです。たとえば、以下のいずれかを行う場合は、実行ループを開始する必要があります。
- ポートまたはカスタム入力ソースを使用して、他のスレッドと通信します。
- スレッド上でタイマーを使用します。
- Cocoa アプリケーションで performSelector... メソッドのいずれかを使用します。
- スレッドを定期的な作業を行うように保持します。
実行ループを使用することを選択した場合、構成と設定は簡単です。しかし、すべてのスレッドプログラミングと同様に、適切な状況で二次スレッドを終了する計画が必要です。スレッドを強制的に終了させるよりも、スレッドをきれいに終了させる方が常に良い方法です。実行ループを構成し、また終了する方法については、実行ループオブジェクトの使用 を参照してください。
実行ループオブジェクトの使用
実行ループオブジェクトは、入力ソース、タイマー、および実行ループ監視者を実行ループに追加して実行するためのメインインターフェイスを提供します。すべてのスレッドには 1 つの実行ループオブジェクトが関連付けられています。Cocoa では、このオブジェクトは NSRunLoop クラスのインスタンスです。低レベルのアプリケーションでは、これは CFRunLoopRef の不透明な型へのポインタです。
実行ループオブジェクトの取得
現在のスレッドの実行ループを取得するには、以下のいずれか 1 つを使用します。
- Cocoa アプリケーションでは、NSRunLoop の currentRunLoop クラスメソッドを使用して NSRunLoop オブジェクトを取得します。
- CFRunLoopGetCurrent 関数を使用します。
通話無料でブリッジされた型ではありませんが、必要に応じて NSRunLoop オブジェクトから CFRunLoopRef 不透明型を取得できます。NSRunLoop クラスは、Core Foundation ルーチンに渡すことができる CFRunLoopRef 型を返す getCFRunLoop メソッドを定義します。両方のオブジェクトが同じ実行ループを参照するため、必要に応じて NSRunLoop オブジェクトと CFRunLoopRef 不透明型への呼び出しを混在させることができます。
実行ループの構成
二次スレッドで実行ループを実行する前に、少なくとも 1 つの入力ソースまたはタイマーを追加する必要があります。実行ループに監視すべきソースがない場合は、実行しようとするとすぐに終了します。実行ループにソースを追加する方法の例については、実行ループソースの構成 を参照してください。
ソースをインストールするだけでなく、実行ループ監視者もインストールして、実行ループのさまざまな実行ステージを検出するためそれを使用することもできます。実行ループ監視者をインストールするには、CFRunLoopObserverRef の不透明な型を作成し、CFRunLoopAddObserver 関数を使用して実行ループに追加します。Cocoa アプリケーションの場合でも、実行ループ監視者は Core Foundation を使用して作成しなければなりません。
リスト 3-1 に、実行ループ監視者を実行ループに接続するスレッドのメインルーチンを示します。この例の目的は、実行ループ監視者を作成する方法を示すことで、このコードは実行ループ監視者を設定してすべての実行ループアクティビティを監視するだけです。基本ハンドラルーチン (図示せず) は、タイマ要求を処理するときに実行ループアクティビティを記録するだけです。
リスト 3-1 実行ループ監視者の作成
- (void)threadMain { // The application uses garbage collection, so no autorelease pool is needed. NSRunLoop* myRunLoop = [NSRunLoop currentRunLoop]; // Create a run loop observer and attach it to the run loop. CFRunLoopObserverContext context = {0, self, NULL, NULL, NULL}; CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, &myRunLoopObserver, &context); if (observer) { CFRunLoopRef cfLoop = [myRunLoop getCFRunLoop]; CFRunLoopAddObserver(cfLoop, observer, kCFRunLoopDefaultMode); } // Create and schedule the timer. [NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:@selector(doFireTimer:) userInfo:nil repeats:YES]; NSInteger loopCount = 10; do { // Run the run loop 10 times to let the timer fire. [myRunLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1]]; loopCount--; } while (loopCount); }
長い寿命のスレッドの実行ループを構成する場合は、少なくとも 1 つの入力ソースを追加してメッセージを受信する方がよいでしょう。タイマーのみを接続して実行ループに入ることはできますが、タイマーが起動すると通常は無効になり、実行ループが終了します。反復するタイマーを接続すると、実行ループを長時間実行することができますが、事実上別の形式のポーリングであるスレッドを起こすためにタイマーを定期的に起動する必要があります。これとは対照的に、入力ソースはイベントが発生するのを待って、イベントが起こるまでスリープ状態を維持します。
実行ループの開始
実行ループの開始は、アプリケーションの二次スレッドに対してのみ必要です。実行ループには、監視すべき少なくとも 1 つの入力ソースまたはタイマーがなければなりません。接続されていない場合は、実行ループがすぐに終了します。
実行ループを開始するには、以下のようないくつかの方法があります。
- 無条件で
- 設定された制限時間で
- 特定のモードで
あなたの実行ループを無条件で入力するのが最も簡単な選択肢ですが、それはまた最も望ましくありません。実行ループを無条件に実行すると、スレッドが永続ループになり、実行ループ自体をほとんど制御できなくなります。入力ソースとタイマーを追加または削除できますが、実行ループを停止する唯一の方法は、それを強制終了することです。カスタムモードで実行ループを実行する方法もありません。
無条件に実行ループを実行する代わりに、実行ループをタイムアウト値で実行する方がよいでしょう。タイムアウト値を使用すると、イベントが到着するか、割り当てられた時間が経過するまで実行ループが実行されます。イベントが到着すると、そのイベントは処理のためにハンドラに急送され、実行ループが終了します。コードは、実行ループを再起動して、次のイベントを処理することができます。割り当てられた時間が来た場合は、実行ループを再起動するか、必要なハウスキーピングを行うために時間を使います。
タイムアウト値に加えて、特定のモードを使用して実行ループを実行することもできます。モードとタイムアウト値は相互に排他的ではなく、実行ループの開始時に両方とも使用できます。モードは、実行ループにイベントを配信するソースの型を制限します。詳細については、実行ループモード を参照してください。
リスト 3-2 は、スレッドのメインエントリルーチンの骨格のバージョンを示しています。この例の重要な部分は、実行ループの基本構造を示しています。本質的には、入力ソースとタイマーを実行ループに追加し、実行ループを開始するためにルーチンの 1 つを繰り返し呼び出します。実行ループルーチンが戻るたびに、スレッドの終了を保証する条件が発生しているかどうかをチェックして下さい。この例では、Core Foundation の実行ループルーチンを使用して、戻って来る結果を確認して、実行ループが終了した理由を判断できるようにしています。NSRunLoop クラスのメソッドを使用して、Cocoa を使用していて戻り値を確認する必要がない場合と同様の方法で実行ループを実行できます。NSRunLoop クラスのメソッドを呼び出す実行ループの例については、リスト 3-14 を参照してください。
リスト 3-2 実行ループの実行
- (void)skeletonThreadMain { // Set up an autorelease pool here if not using garbage collection. BOOL done = NO; // Add your sources or timers to the run loop and do any other setup. do { // Start the run loop but return after each source is handled. SInt32 result = CFRunLoopRunInMode(kCFRunLoopDefaultMode, 10, YES); // If a source explicitly stopped the run loop, or if there are no // sources or timers, go ahead and exit. if ((result == kCFRunLoopRunStopped) || (result == kCFRunLoopRunFinished)) done = YES; // Check for any other exit conditions here and set the // done variable as needed. } while (!done); // Clean up code here. Be sure to release any allocated autorelease pools. }
実行ループを再帰的に実行することは可能です。つまり、CFRunLoopRun、CFRunLoopRunInMode、または NSRunLoop メソッドのいずれかを呼び出して、入力ソースまたはタイマーのハンドラルーチン内から実行ループを開始することができます。そうするときは、外側の実行ループで使用されているモードを含め、ネストされた実行ループを実行する任意のモードを使用できます。
実行ループの終了
実行ループがイベントを処理する前に実行ループを終了するには、以下の 2 つの方法があります。
- 実行ループをタイムアウト値で実行するように構成します。
- 実行ループを停止するように指示します。
それを管理できるなら、タイムアウト値を使うことは確かに望ましいことです。タイムアウト値を指定すると、実行ループ監視者を終了する前に、ループ監視者を実行するように通知を送信することを含め、実行ループは通常の処理をすべて終了します。
CFRunLoopStop 関数を使用して実行ループを明示的に停止すると、タイムアウトと同様の結果が得られます。実行ループは、残りの実行ループ通知を送信してから終了します。違いは、無条件で開始した実行ループでこの手法を使用できることです。
実行ループの入力ソースとタイマーを削除すると実行ループが終了することもありますが、これは実行ループを停止する信頼できる方法ではありません。システム・ルーチンの中には、入力ソースを実行ループに追加して必要なイベントを処理するものがあります。コードがこれらの入力ソースを認識していない可能性があり、コードを削除できないため、実行ループが終了する事から防止されています。
スレッドの安全性と実行ループオブジェクト
スレッドの安全性は、実行ループを操作するために使用している API によって異なります。Core Foundation の関数は一般にスレッドセーフであり、任意のスレッドから呼び出すことができます。ただし、実行ループの構成を変更する操作を実行する場合は、可能な限り実行ループを所有するスレッドから実行することをお勧めします。
Cocoa の NSRunLoop クラスは、Core Foundation での相手のように本質的にスレッドセーフではありません。NSRunLoop クラスを使用して実行ループを変更する場合は、その実行ループを所有する同じスレッドからのみ実行する必要があります。別のスレッドに属する実行ループに入力ソースまたはタイマーを追加すると、コードがクラッシュしたり予期しない動作をする可能性があります。
実行ループソースの構成
以下のセクションでは、Cocoa と Core Foundation の両方でさまざまな型の入力ソースを設定する方法の例を示します。
カスタム入力ソースの定義
カスタム入力ソースを作成するには、以下の定義が必要です。
- 入力ソースで処理したい情報。
- 関心のあるクライアントに入力ソースへの連絡方法を知らせるスケジューラルーチン。
- 任意のクライアントから送信された要求を実行するハンドラルーチン。
- 入力ソースを無効にする取り消しルーチン。
カスタム情報を処理するカスタム入力ソースを作成するので、実際の構成は柔軟に設計されています。スケジューラー、ハンドラー、および取り消しルーチンは、カスタム入力ソースにほとんど常に必要なキールーチンです。ただし、残りの入力ソース動作のほとんどは、それらのハンドラルーチンの外側で発生します。たとえば、入力ソースにデータを渡したり、入力ソースの存在を他のスレッドに伝えるための仕組みを定義するのはあなた次第です。
図 3-2 は、カスタム入力ソースのサンプル構成を示しています。この例では、アプリケーションのメインスレッドは、入力ソース、その入力ソースのカスタム指令バッファ、および入力ソースがインストールされている実行ループへの参照を保持します。メインスレッドは、ワーカースレッドに渡したいタスクを持っているとき、タスクを開始するためにワーカースレッドが必要とする全ての情報とともに指令バッファに指令を送信します。(メインスレッドとワーカースレッドの入力ソースの両方が指令バッファにアクセスできるため、そのアクセスは同期されなければなりません。) 指令が送信されると、メインスレッドは入力ソースに信号を送り、ワーカースレッドの実行ループを覚醒します。覚醒の指令を受信すると、実行ループは入力ソース用のハンドラを呼び出し、指令バッファにある指令を処理します。
図 3-2 カスタム入力ソースの操作
以下のセクションでは、前の図のカスタム入力ソースの実装について説明し、実装する必要があるキーコードを示します。
入力ソースの定義
カスタム入力ソースを定義するには、実行ループソースを構成して実行ループに接続するために、Core Foundation ルーチンを使用する必要があります。基本的なハンドラは C を基本とした関数ですが、それらの関数のラッパーを記述したり、Objective-C または C++ を使用してコードの本体を実装することを妨げるものではありません。
図 3-2 で紹介した入力ソースは、Objective-C オブジェクトを使用して指令バッファを管理し、実行ループと連携します。リスト 3-3 に、このオブジェクトの定義を示します。RunLoopSource オブジェクトは指令バッファを管理し、そのバッファを使用して他のスレッドからメッセージを受信します。このリストは RunLoopContext オブジェクトの定義も示しています。これは実際には RunLoopSource オブジェクトと実行ループ参照をアプリケーションのメインスレッドに渡すために使用されるコンテナオブジェクトに過ぎません。
リスト 3-3 カスタム入力ソースオブジェクトの定義
@interface RunLoopSource : NSObject { CFRunLoopSourceRef runLoopSource; NSMutableArray* commands; } - (id)init; - (void)addToCurrentRunLoop; - (void)invalidate; // Handler method - (void)sourceFired; // Client interface for registering commands to process - (void)addCommand:(NSInteger)command withData:(id)data; - (void)fireAllCommandsOnRunLoop:(CFRunLoopRef)runloop; @end // These are the CFRunLoopSourceRef callback functions. void RunLoopSourceScheduleRoutine (void *info, CFRunLoopRef rl, CFStringRef mode); void RunLoopSourcePerformRoutine (void *info); void RunLoopSourceCancelRoutine (void *info, CFRunLoopRef rl, CFStringRef mode); // RunLoopContext is a container object used during registration of the input source. @interface RunLoopContext : NSObject { CFRunLoopRef runLoop; RunLoopSource* source; } @property (readonly) CFRunLoopRef runLoop; @property (readonly) RunLoopSource* source; - (id)initWithSource:(RunLoopSource*)src andLoop:(CFRunLoopRef)loop; @end
Objective-C コードは入力ソースのカスタムデータを管理しますが、入力ソースを実行ループに接続するには、C を基本とした呼び出し関数が必要です。これらの関数の 1 つ目は、実行ループソースを実際に実行ループに接続するときに呼び出され、リスト 3-4 に示されています。この入力ソースにはクライアント (メインスレッド) が 1 つしかないため、スケジューラー関数を使用して、そのスレッドのアプリケーションデリゲートに自身を登録するメッセージを送信します。デリゲートが入力ソースと通信したい場合、デリゲートは RunLoopContext オブジェクトの情報を使用してそれを行います。
リスト 3-4 実行ループソースのスケジューリング
void RunLoopSourceScheduleRoutine (void *info, CFRunLoopRef rl, CFStringRef mode) { RunLoopSource* obj = (RunLoopSource*)info; AppDelegate* del = [AppDelegate sharedAppDelegate]; RunLoopContext* theContext = [[RunLoopContext alloc] initWithSource:obj andLoop:rl]; [del performSelectorOnMainThread:@selector(registerSource:) withObject:theContext waitUntilDone:NO]; }
最も重要な呼び出しルーチンの 1 つは、入力ソースが通知されたときにカスタムデータを処理するためのルーチンです。リスト 3-5 は、RunLoopSource オブジェクトに関連した実行呼び出しルーチンを示しています。この関数は、作業を行う要求を単に sourceFired メソッドに転送し、それは指令バッファに存在するすべての指令を処理します。
リスト 3-5 入力ソースでの作業の実行
void RunLoopSourcePerformRoutine (void *info) { RunLoopSource* obj = (RunLoopSource*)info; [obj sourceFired]; }
CFRunLoopSourceInvalidate 関数を使用してその実行ループから入力ソースを削除した場合、システムは入力ソースの取り消しルーチンを呼び出します。このルーチンを使用して、入力ソースがもはや有効でなく、入力ソースへの参照を削除する必要があることをクライアントに通知できます。リスト 3-6 に、RunLoopSource オブジェクトに登録されている取り消しの呼び出しルーチンを示します。この関数は、別の RunLoopContext オブジェクトをアプリケーションデリゲートに送信しますが、今回はデリゲートに実行ループソースへの参照を削除するように要求します。
リスト 3-6 入力ソースの取り消し
void RunLoopSourceCancelRoutine (void *info, CFRunLoopRef rl, CFStringRef mode) { RunLoopSource* obj = (RunLoopSource*)info; AppDelegate* del = [AppDelegate sharedAppDelegate]; RunLoopContext* theContext = [[RunLoopContext alloc] initWithSource:obj andLoop:rl]; [del performSelectorOnMainThread:@selector(removeSource:) withObject:theContext waitUntilDone:YES]; }
実行ループへの入力ソースのインストール
リスト 3-7 に、RunLoopSource クラスの init メソッドと addToCurrentRunLoop メソッドを示します。init メソッドは、実際に実行ループに所属しなければならない不透明な CFRunLoopSourceRef 型を作成します。呼び出しルーチンがオブジェクトへのポインタを持つように、コンテキスト情報として RunLoopSource オブジェクト自体を渡します。ワーカースレッドが addToCurrentRunLoop メソッドを呼び出すまで、入力ソースのインストールは実行されず、この時点で、RunLoopSourceScheduleRoutine 呼び出し関数が呼び出されます。入力ソースが実行ループに追加されると、スレッドは実行ループを実行してスレッド上に待機させることができます。
リスト 3-7 実行ループソースのインストール
- (id)init { CFRunLoopSourceContext context = {0, self, NULL, NULL, NULL, NULL, NULL, &RunLoopSourceScheduleRoutine, RunLoopSourceCancelRoutine, RunLoopSourcePerformRoutine}; runLoopSource = CFRunLoopSourceCreate(NULL, 0, &context); commands = [[NSMutableArray alloc] init]; return self; } - (void)addToCurrentRunLoop { CFRunLoopRef runLoop = CFRunLoopGetCurrent(); CFRunLoopAddSource(runLoop, runLoopSource, kCFRunLoopDefaultMode); }
入力ソースのクライアントとの調整
入力ソースを使えるようにするには、入力ソースを操作して別のスレッドから通知する必要があります。入力ソースの全体のポイントは、何かする事があるまで関連するスレッドをスリープ状態にすることです。そのためには、アプリケーション内の他のスレッドに入力ソースについて知らせ、それと通信する方法が必要です。
入力ソースについてクライアントに通知する 1 つの方法は、入力ソースが最初に実行ループにインストールされたときに登録要求を送信することです。入力ソースは、必要な数のクライアントで登録することもできますし、セントラルエージェンシーに単に登録して、入力ソースを関心のあるクライアントに提供することもできます。リスト 3-8 は、アプリケーションデリゲートによって定義され、RunLoopSource オブジェクトのスケジューラー関数が呼び出されたときに呼び出される登録メソッドを示しています。このメソッドは、RunLoopSource オブジェクトによって提供される RunLoopContext オブジェクトを受け取り、それをソースのリストに追加します。このリストには、入力ソースが実行ループから削除されたときに入力ソースの登録を解除するために使用されるルーチンも示しています。
リスト 3-8 アプリケーションデリゲートによる入力ソースの登録と削除
- (void)registerSource:(RunLoopContext*)sourceInfo; { [sourcesToPing addObject:sourceInfo]; } - (void)removeSource:(RunLoopContext*)sourceInfo { id objToRemove = nil; for (RunLoopContext* context in sourcesToPing) { if ([context isEqual:sourceInfo]) { objToRemove = context; break; } } if (objToRemove) [sourcesToPing removeObject:objToRemove]; }
入力ソースへの信号
データを入力ソースに渡した後、クライアントはソースに信号を送り、その実行ループを起動しなければなりません。ソースに信号を送ることにより、ソースが処理される準備ができていることを実行ループに知らせることができます。また、信号が発生したときにスレッドがスリープ状態になる可能性があるため、常に実行ループを明示的に覚醒させる必要があります。そうしないと、入力ソースの処理が遅れることがあります。
リスト 3-9 に、RunLoopSource オブジェクトの fireCommandsOnRunLoop メソッドを示します。クライアントは、バッファに追加した指令をソースが処理できる状態になったら、このメソッドを呼び出します。
リスト 3-9 実行ループを覚醒させる
- (void)fireCommandsOnRunLoop:(CFRunLoopRef)runloop { CFRunLoopSourceSignal(runLoopSource); CFRunLoopWakeUp(runloop); }
タイマーソースの構成
タイマーソースを作成するには、タイマーオブジェクトを作成して実行ループでスケジュールを立てるだけです。Cocoa では、NSTimer クラスを使用して新しいタイマーオブジェクトを作成し、Core Foundation では CFRunLoopTimerRef の不透明型を使用します。内部的には、NSTimer クラスは、同じメソッドを使用してタイマーを作成およびスケジュールを立てる機能など、便利な機能を提供する Core Foundation の拡張版です。
Cocoa では、以下のいずれかのクラスメソッドを使用して、タイマーを一度に作成してスケジュールを立てることができます。
- scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:
- scheduledTimerWithTimeInterval:invocation:repeats:
これらのメソッドは、タイマーを作成し、現在のスレッドの実行ループをデフォルトモード (NSDefaultRunLoopMode) で追加します。NSTimer オブジェクトを作成し、それを NSRunLoop の addTimer:forMode: メソッドを使用して実行ループに追加することによって、タイマーを手動でスケジュール化することもできます。両方のテクニックは基本的に同じことを行いますが、タイマーの構成をさまざまなレベルで制御できます。たとえば、タイマーを作成してそれを実行ループに手動で追加する場合は、デフォルトモード以外のモードを使用することができます。リスト 3-10 に、両方の手法を使ってタイマーを作成する方法を示します。最初のタイマーの初期遅延は 1 秒ですが、その後 0.1 秒ごとに定期的に起動します。第 2 のタイマーは、最初の 0.2 秒の遅延の後に起動を開始し、その後 0.2 秒ごとに起動します。
リスト 3-10 NSTimer を使用したタイマーの作成とスケジュール化
NSRunLoop* myRunLoop = [NSRunLoop currentRunLoop]; // Create and schedule the first timer. NSDate* futureDate = [NSDate dateWithTimeIntervalSinceNow:1.0]; NSTimer* myTimer = [[NSTimer alloc] initWithFireDate:futureDate interval:0.1 target:self selector:@selector(myDoFireTimer1:) userInfo:nil repeats:YES]; [myRunLoop addTimer:myTimer forMode:NSDefaultRunLoopMode]; // Create and schedule the second timer. [NSTimer scheduledTimerWithTimeInterval:0.2 target:self selector:@selector(myDoFireTimer2:) userInfo:nil repeats:YES];
リスト 3-11 に、Core Foundation 関数を使用してタイマーを構成するために必要なコードを示します。この例では、コンテキスト構造体でユーザー定義の情報を渡すことはありませんが、この構造体を使用してタイマーに必要なカスタムデータを渡すことができます。この構造体のコンテキストの詳細については、CFRunLoopTimer リファレンス の説明を参照してください。
リスト 3-11 Core Foundation を使用したタイマーの作成とスケジュール化
CFRunLoopRef runLoop = CFRunLoopGetCurrent(); CFRunLoopTimerContext context = {0, NULL, NULL, NULL, NULL}; CFRunLoopTimerRef timer = CFRunLoopTimerCreate(kCFAllocatorDefault, 0.1, 0.3, 0, 0, &myCFTimerCallback, &context); CFRunLoopAddTimer(runLoop, timer, kCFRunLoopCommonModes);
ポートを基本とした入力ソースの構成
Cocoa と Core Foundation はどちらも、スレッド間またはプロセス間で通信するためのポートを基本としたオブジェクトを提供します。以下のセクションでは、いくつかの異なる型のポートを使用してポート通信を設定する方法を示します。
NSMachPort オブジェクトの構成
NSMachPort オブジェクトとのローカル接続を確立するには、ポートオブジェクトを作成し、それを一時スレッドの実行ループに追加します。二時スレッドを起動するとき、スレッドのエントリポイント関数に同じオブジェクトを渡して下さい。二次スレッドは、同じオブジェクトを使用して一次スレッドにメッセージを送り返すことができます。
メインスレッドコードの実装
リスト 3-12 に、二次ワーカースレッドを起動するための一次スレッドコードを示します。Cocoa フレームワークは、ポートと実行ループを構成するための介在する手順の多くを実行するため、launchThread メソッドは、Core Foundation の同等物 (リスト 3-17) よりもはるかに短いです。しかし、2 つの動作はほぼ同じです。1 つの違いは、ローカルポートの名前をワーカースレッドに送信する代わりに、このメソッドは NSPort オブジェクトを直接送信することです。
リスト 3-12 メインスレッドの起動メソッド
- (void)launchThread { NSPort* myPort = [NSMachPort port]; if (myPort) { // This class handles incoming port messages. [myPort setDelegate:self]; // Install the port as an input source on the current run loop. [[NSRunLoop currentRunLoop] addPort:myPort forMode:NSDefaultRunLoopMode]; // Detach the thread. Let the worker release the port. [NSThread detachNewThreadSelector:@selector(LaunchThreadWithPort:) toTarget:[MyWorkerClass class] withObject:myPort]; } }
スレッド間の双方向通信チャネルを設定するには、ワーカースレッドが自分のローカルポートをメインスレッドにチェックインメッセージで送信させたい場合があります。チェックインメッセージを受信すると、メインスレッドは 2 番目のスレッドの起動にうまくいっていることを知ることができます。また、そのスレッドにさらにメッセージを送信する方法を提供します。
リスト 3-13 に、一次スレッド用の handlePortMessage: メソッドを示します。このメソッドは、スレッド自身のローカルポートにデータが到着したときに呼び出されます。チェックインメッセージが到着すると、メソッドはポートメッセージから直接二次スレッドのポートを取得し、後で使用できるように保存します。
リスト 3-13 Mach ポートメッセージの処理
#define kCheckinMessage 100 // Handle responses from the worker thread. - (void)handlePortMessage:(NSPortMessage *)portMessage { unsigned int message = [portMessage msgid]; NSPort* distantPort = nil; if (message == kCheckinMessage) { // Get the worker thread’s communications port. distantPort = [portMessage sendPort]; // Retain and save the worker port for later use. [self storeDistantPort:distantPort]; } else { // Handle other messages. } }
二次スレッドコードの実装
二次ワーカースレッドの場合は、スレッドを構成し、指定されたポートを使用して情報を一次スレッドに返信しなければなりません。
リスト 3-14 は、ワーカースレッドを設定するコードを示しています。スレッドの自動解放プールを作成した後、メソッドはスレッドの実行を駆動するワーカーオブジェクトを作成します。ワーカー・オブジェクトの sendCheckinMessage: メソッド (リスト 3-15 を参照) は、ワーカースレッド用のローカル・ポートを作成し、メインスレッドにチェックインメッセージを返します。
リスト 3−14 Mach ポートを使用したワーカースレッドの起動
+(void)LaunchThreadWithPort:(id)inData { NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init]; // Set up the connection between this thread and the main thread. NSPort* distantPort = (NSPort*)inData; MyWorkerClass* workerObj = [[self alloc] init]; [workerObj sendCheckinMessage:distantPort]; [distantPort release]; // Let the run loop process things. do { [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]; } while (![workerObj shouldExit]); [workerObj release]; [pool release]; }
NSMachPort を使用する場合、ローカルスレッドとリモートスレッドは、スレッド間の一方向通信に同じポートオブジェクトを使用できます。つまり、一方のスレッドによって作成されたローカルポートオブジェクトは、他方のスレッドのリモートポートオブジェクトになります。
リスト 3-15 に、二次スレッドのチェックインルーチンを示します。このメソッドは、将来の通信のために独自のローカルポートを設定し、メインスレッドにチェックインメッセージを返します。 このメソッドは、 LaunchThreadWithPort: メソッドで受信したポートオブジェクトをメッセージのターゲットとして使用します。
リスト 3-15 Mach ポートを使用してチェックインメッセージを送信
// Worker thread check-in method - (void)sendCheckinMessage:(NSPort*)outPort { // Retain and save the remote port for future use. [self setRemotePort:outPort]; // Create and configure the worker thread port. NSPort* myPort = [NSMachPort port]; [myPort setDelegate:self]; [[NSRunLoop currentRunLoop] addPort:myPort forMode:NSDefaultRunLoopMode]; // Create the check-in message. NSPortMessage* messageObj = [[NSPortMessage alloc] initWithSendPort:outPort receivePort:myPort components:nil]; if (messageObj) { // Finish configuring the message and send it immediately. [messageObj setMsgId:setMsgid:kCheckinMessage]; [messageObj sendBeforeDate:[NSDate date]]; } }
NSMessagePort オブジェクトの構成
NSMessagePort オブジェクトとのローカル接続を確立するには、単にスレッド間でポートオブジェクトを渡すことではできません。リモートメッセージポートは名前で取得しなければなりません。これを Cocoa で可能にするには、ローカルポートに特定の名前を登録し、その名前をリモートスレッドに渡して通信用の適切なポートオブジェクトを取得できるようにする必要があります。リスト 3-16 に、メッセージポートを使用する場合のポートの作成と登録プロセスを示します。
リスト 3-16 メッセージポートの登録
NSPort* localPort = [[NSMessagePort alloc] init]; // Configure the object and add it to the current run loop. [localPort setDelegate:self]; [[NSRunLoop currentRunLoop] addPort:localPort forMode:NSDefaultRunLoopMode]; // Register the port using a specific name. The name must be unique. NSString* localPortName = [NSString stringWithFormat:@"MyPortName"]; [[NSMessagePortNameServer sharedInstance] registerPort:localPort name:localPortName];
Core Foundation でのポートを基本とした入力ソースの構成
このセクションでは、Core Foundation を使用して、アプリケーションのメインスレッドとワーカースレッドの間に双方向通信チャネルを設定する方法を示します。
リスト 3-17 に、アプリケーションのメインスレッドがワーカースレッドを起動するために呼び出すコードを示します。 コードが最初に行うことは、ワーカースレッドからのメッセージを注視するための不透明な CFMessagePortRef 型を設定することです。ワーカースレッドは、接続を行うためにポートの名前を必要とするため、文字列値はワーカースレッドのエントリポイント関数に渡されます。ポート名は、通常、現在のユーザーコンテキスト内で一意である必要があります。それ以外の場合は、競合 (コンフリクト) が発生する可能性があります。
リスト 3-17 Core Foundation メッセージポートを新しいスレッドに接続する
#define kThreadStackSize (8 *4096) OSStatus MySpawnThread() { // Create a local port for receiving responses. CFStringRef myPortName; CFMessagePortRef myPort; CFRunLoopSourceRef rlSource; CFMessagePortContext context = {0, NULL, NULL, NULL, NULL}; Boolean shouldFreeInfo; // Create a string with the port name. myPortName = CFStringCreateWithFormat(NULL, NULL, CFSTR("com.myapp.MainThread")); // Create the port. myPort = CFMessagePortCreateLocal(NULL, myPortName, &MainThreadResponseHandler, &context, &shouldFreeInfo); if (myPort != NULL) { // The port was successfully created. // Now create a run loop source for it. rlSource = CFMessagePortCreateRunLoopSource(NULL, myPort, 0); if (rlSource) { // Add the source to the current run loop. CFRunLoopAddSource(CFRunLoopGetCurrent(), rlSource, kCFRunLoopDefaultMode); // Once installed, these can be freed. CFRelease(myPort); CFRelease(rlSource); } } // Create the thread and continue processing. MPTaskID taskID; return(MPCreateTask(&ServerThreadEntryPoint, (void*)myPortName, kThreadStackSize, NULL, NULL, NULL, 0, &taskID)); }
ポートがインストールされ、スレッドが起動されると、スレッドがチェックインするのを待つ間、メインスレッドは通常の実行を継続できます。チェックインメッセージが到着すると、メインスレッドの MainThreadResponseHandler 関数に送出されます (リスト 3-18)。この関数は、ワーカースレッドのポート名を抽出し、将来の通信のため道を作成します。
リスト 3-18 チェックインメッセージの受領
#define kCheckinMessage 100 // Main thread port message handler CFDataRef MainThreadResponseHandler(CFMessagePortRef local, SInt32 msgid, CFDataRef data, void* info) { if (msgid == kCheckinMessage) { CFMessagePortRef messagePort; CFStringRef threadPortName; CFIndex bufferLength = CFDataGetLength(data); UInt8* buffer = CFAllocatorAllocate(NULL, bufferLength, 0); CFDataGetBytes(data, CFRangeMake(0, bufferLength), buffer); threadPortName = CFStringCreateWithBytes (NULL, buffer, bufferLength, kCFStringEncodingASCII, FALSE); // You must obtain a remote message port by name. messagePort = CFMessagePortCreateRemote(NULL, (CFStringRef)threadPortName); if (messagePort) { // Retain and save the thread’s comm port for future reference. AddPortToListOfActiveThreads(messagePort); // Since the port is retained by the previous function, release // it here. CFRelease(messagePort); } // Clean up. CFRelease(threadPortName); CFAllocatorDeallocate(NULL, buffer); } else { // Process other messages. } return NULL; }
メインスレッドが構成されている場合、残っている事は、新しく作成されたワーカースレッドが独自のポートを作成してチェックインすることだけです。リスト 3-19 に、ワーカースレッドのエントリポイント関数を示します。この関数は、メインスレッドのポート名を抽出し、それを使ってメインスレッドに戻ってリモート接続を作成します。この関数は、それ自身のローカルポートを作成し、スレッドの実行ループにポートをインストールし、チェックインメッセージをローカルポート名を含むメインスレッドに送信します。
リスト 3-19 スレッド構造体の設定
OSStatus ServerThreadEntryPoint(void* param) { // Create the remote port to the main thread. CFMessagePortRef mainThreadPort; CFStringRef portName = (CFStringRef)param; mainThreadPort = CFMessagePortCreateRemote(NULL, portName); // Free the string that was passed in param. CFRelease(portName); // Create a port for the worker thread. CFStringRef myPortName = CFStringCreateWithFormat(NULL, NULL, CFSTR("com.MyApp.Thread-%d"), MPCurrentTaskID()); // Store the port in this thread’s context info for later reference. CFMessagePortContext context = {0, mainThreadPort, NULL, NULL, NULL}; Boolean shouldFreeInfo; Boolean shouldAbort = TRUE; CFMessagePortRef myPort = CFMessagePortCreateLocal(NULL, myPortName, &ProcessClientRequest, &context, &shouldFreeInfo); if (shouldFreeInfo) { // Couldn't create a local port, so kill the thread. MPExit(0); } CFRunLoopSourceRef rlSource = CFMessagePortCreateRunLoopSource(NULL, myPort, 0); if (!rlSource) { // Couldn't create a local port, so kill the thread. MPExit(0); } // Add the source to the current run loop. CFRunLoopAddSource(CFRunLoopGetCurrent(), rlSource, kCFRunLoopDefaultMode); // Once installed, these can be freed. CFRelease(myPort); CFRelease(rlSource); // Package up the port name and send the check-in message. CFDataRef returnData = nil; CFDataRef outData; CFIndex stringLength = CFStringGetLength(myPortName); UInt8* buffer = CFAllocatorAllocate(NULL, stringLength, 0); CFStringGetBytes(myPortName, CFRangeMake(0,stringLength), kCFStringEncodingASCII, 0, FALSE, buffer, stringLength, NULL); outData = CFDataCreate(NULL, buffer, stringLength); CFMessagePortSendRequest(mainThreadPort, kCheckinMessage, outData, 0.1, 0.0, NULL, NULL); // Clean up thread data structures. CFRelease(outData); CFAllocatorDeallocate(NULL, buffer); // Enter the run loop. CFRunLoopRun(); }
実行ループに入ると、スレッドのポートに送られる将来のすべてのイベントは、ProcessClientRequest 関数によって処理されます。その関数の実装は、スレッドが行う作業の型によって異なり、ここでは示されていません。