 |
Technote 1071
Working with Multiprocessing Services
Apple マルチプロセッシング
API について
Apple マルチプロセッシング API は、アプリケーションがタスクと呼ばれる実行の独立したスレッドを作成することを可能にする呼び出しのセットを提供します。タスクは、システム内で使用可能なプロセッサ上でプリエンプティブにスケジュールされます。これは、使用可能なプロセッサが一つしかない場合も同様です。タスクは、システムメモリについてアプリケーションと同様のビューを持ちます。
アプリケーションでは、アプリケーションに代わって処理を実行するために使用するタスクを作成します。マルチプロセッサシステムでは、タスクによって処理は同時に実行されるため、アプリケーションの処理のスループットは飛躍的に向上します。
たとえば、イメージ処理アプリケーションは、イメージデータの任意のブロックを変形するタスクを作成することができます。ユーザがイメージの変形を選択するとき、アプリケーションはデータを複数の小さな部分に分割し、それぞれのタスクが一つの部分を変形するように要求します。このプロセスは、すべての部分が変形されるまで繰り返されます。それぞれの部分は同時に処理されるため、イメージ全体を変更するのに要する時間は非常に短縮されます。
別の例としては開発環境があります。開発環境では、タスクを使って、異なる複数のファイルをコンパイルすることができます。この方法を使うと、複数のファイルを同時にコンパイルできるため、プロジェクトのビルドに要する時間は大幅に短縮されます。
マルチプロセッシングの使い方
ライブラリの使い方
マルチプロセッシングを使用するには、マルチプロセッシング API ライブラリをプロジェクトに追加して、弱いリンクを設定します。Metrowerks
でライブラリと弱いリンクを設定するには、プロジェクトウィンドウのライブラリ名の隣にあるプルダウンメニューを使用し、“weak
link”を選択します。この操作により、ライブラリが存在しない場合でも、アプリケーションは起動されるようになります。
アプリケーションの先頭近くで、マルチプロセッシング API ライブラリの存在をテストしてください。この処理を行うファイルには、MP.h
ファイルをインクルードします。ライブラリの存在をテストするには、MPLibraryIsLoaded
を呼び出します。MPLibraryIsLoaded は、ライブラリのエントリポイントのいずれかの存在をテストするマクロです。MPLibraryIsLoaded
の戻り値が True の場合、ライブラリは存在し、そのサービスを使用することができます。戻り値が
False の場合、アプリケーションではマルチプロセッシングサービスを呼び出すことなく実行されることになります。
作成するタスク数の決定
システム内のプロセッサ数をカウントするには MPProcessors を使用します。プロセッサが一つしかない場合は、マルチプロセッシングサービスが使用可能でないかのように処理を続行します。ただし、必要な場合は、シングルプロセッサ環境でもプリエンプティブなタスクを作成することができます。
MPProcessors から返されたカウントは、通常、作成するタスク数の指標として使用します。作成するタスク数を決定するにはいくつかの要因が存在します。
まず第一の要因は、アプリケーションがすべてのプロセッサをビジーな状態にし続けようとするかどうかです。これを行うための最も簡単な方法は、少なくともプロセッサと同数のタスクを作成することです。こうすれば、アプリケーションは処理をその数の部分に分割し、それぞれのタスクが一つの部分を処理することを要求します。アプリケーションでは、処理を続行する前に、タスクが終了するのを待ちます。タスクがすべての部分を終了するために
1/10 秒以上を要する場合、アプリケーションでは、タスクの完了を待つ一方で、イベントのチェックを行ってください。
上の方法の代用として頻繁に採用されるテクニックは、存在するプロセッサの数よりも一つ少ないタスクを作成するという方法です。この場合、アプリケーションは処理をプロセッサと同数の部分に分割します。それぞれのタスクは一つの部分を処理するように要求され、残された部分はメインアプリケーションが直接処理します。処理が終了すると、アプリケーションはタスクが終了するのを待ちます。処理が適切に等分に分割されている場合、アプリケーションが待機を開始する時点で、タスクは終了しているか、ほぼ終了しようとしているはずです。このアプローチは、シングルプロセッサ環境に容易に一般化できるため非常に人気があります。つまり、作成されるタスクの数をゼロにすると、当然のことながらアプリケーションが最終的にすべての処理を実行することになります。また、このアプローチは、何らかの理由でマルチプロセッシング
API ライブラリが使用できないときにしたがう経路でもあります。このとき、アプリケーションは処理の実行にかかわるため、処理の実行に要する時間を
1/10 秒未満に制限することが重要になります。こうすれば、イベントのチェックを十分な頻度で実行することができ、アプリケーションはその責任を果たすことができます。
上のケースでは、すべてのタスクがほぼ同じ処理を実行することが前提になっています。アプリケーションがまったく異なるタイプの処理を実行するタスクを作成する場合、作成するタスクの数は、実行する必要のある処理のタイプによってまったく異なります。留意すべき重要なポイントは、プロセッサをできるかぎりビジーにし続けることです。これを行うには、スループットを最大化しようとする間は、少なくともプロセッサと同数のタスクが関与するようにします。4
プロセッサシステムも一般化しつつありますが、将来はさらに強力なシステムの登場が予想されます。
タスクとのデータのやり取り
アプリケーションとタスクとの間のデータのやり取りは、共有メモリおよび同期メソッドという
2 つの基本的な方法で実行されます。すべてのメモリは共有されているため、アプリケーションがメモリに書き込んだデータはすべてタスクからも使用できます。もちろんこの逆も可能です。しかし、アプリケーションによって用意されたメモリにアクセスする前に、タスクは、マルチプロセッシング
API ライブラリで使用可能な 3 つのメソッドのいずれかを使って、アプリケーションと同期する必要があります。このことは非常に重要です。PowerPC
アーキテクチャでは、メモリへの書き込みを遅延することができます。これは、PowerPC の非常に高速な処理速度を実現するために役立つリソース管理機能の一つです。別のプロセッサがメモリ内の正しい値を読み込みためには、ある種のハードウェアに依存した命令を実行する必要があります。タスクが同期メソッドを使用するとき、これらの命令が実行され、それ以降、処理に関与するプロセッサは確実に一貫したメモリのビューを持つようになります。何らかの理由でコミュニカントのいずれかで同期するための準備がまだ整っていないときに、他のコミュニカントがそれに対するプロセッサに処理を譲渡できるようにするためにも、同期メソッドを使用することは重要です。これにより、プロセッサは即座に他のタスクでも使用可能になり、もっと効率的にプロセッサを使用することが可能になります。
マルチプロセッシング API ライブラリで使用可能な同期メソッドは、Queue、Semaphore、および
Critical Region の 3 つです。
Queue は、96 ビットのメッセージからなる先入れ先出しのキューです。要素の挿入と抽出はアトミックオペレーションであり、多数のタスクが与えられたキューから次のメッセージを抽出しようと試みることはできますが、正常に次のメッセージを取得できるのは一つのタスクだけです。
Semaphore は、単一の 32 ビット値を表し、この値はあらかじめ定義されている最大数までアトミックにインクリメントされ、または最小値のゼロまでアトミックにデクリメントされます。
Critical Region は、それらが囲んでいるコードのセクションが複数のタスクまたはアプリケーションによって同時に実行されないようにします。
タスクを作成する前に、通常は、それらを同期させるための方法を作成することをお勧めします。Queue
と Semaphore は、最も一般的に使用される 2 つのメソッドです。マルチプロセッシングにとりかかろうとする人は、一般に
Queue を使用します。これは、Queue が非常に柔軟で、かつ比較的理解しやすいためです。Semaphore
は Queue と比較すると、より高速でメモリに対する負荷も小さいですが、同程度の柔軟性を提供することはありません。Queue
と Semaphore は、通常ペアで作成されます。つまり、一方がリクエストのシグナルを送り、もう一方が結果のシグナルを送ります。初心者が犯しがちな誤りは
(これは著者の経験から言えることですが)、一方の同期オブジェクトだけを作成し、両方の目的でそれを使用しようとすることです。これでは正常に動作しません。リクエストがポストされた後、いずれかの時点でアプリケーションは結果を待ち始めます。リクエストがポストされたのと同じ場所でアプリケーションが待機を行う場合、リクエストそのものが結果として現れます。アプリケーションは、それが結果であるという誤った認識からリクエストをクリアしてしまうため、まったく何の処理も実行されません。これが、双方向のデータのやり取りを行うために
2 つのまったく異なるエンティティを使用することが重要であるということの理由です。
タスクの作成
タスクの作成は、MPCreateTask を呼び出すことによって実行します。この呼び出しに対する最初のパラメータは、実行タスクとなる関数へのポインタです。タスクの関数は、次のようなプロトタイプを持つ必要があります。
OSStatus fTask( void *parameter );
タスクは、そのスタートアップ時に 32 ビットパラメータを受け取り、その終了時には 32
ビットの結果を返します。スタートアップ時にタスクが受け取るパラメータは、MPCreateTask
呼び出しに対する第 2 のパラメータとして指定します。このパラメータを介して、タスクが必要とするすべての初期情報のやり取りが行われます。この中には、メッセージキューの
ID や C++ オブジェクトへのポインタなど、あらゆるものが含まれています。これが、アプリケーションがタスクの寿命全体にわたってさまざまな情報をやり取りするタスク固有のメモリブロックへのポインタであることも稀ではありません。
MPLibraryIsLoaded の呼び出しから MPCreateTask の呼び出しまで、ここまでのすべての処理は、アプリケーションのスタートアップ時に実行することができます。アプリケーションが実行されている間、タスクを実行し続けることは、必要に応じてタスクを作成して破壊することに比べるとはるかに効率のよい戦略といえます。タイプの異なる処理を実行するさまざまなタスクを多数作成する必要がある場合は、セレクタベースのスキーマ、または可変関数ポインタを介して、タイプの異なる多数の関数を呼び出すことのできるタスクの作成を検討してください。
次のコードは、アプリケーションがその起動直後にマルチプロセッシング機能を確立する方法を具体的に示しています。このコードでは、プロセッサ数よりも一つ少ないタスクを作成するテクニックを使用しています。
|
typedef struct {
long firstThing;
long totalThings;
}sWorkParams, *sWorkParamsPtr;
typedef struct {
MPTaskID taskID;
MPQueueID requestQueue;
MPQueueID resultQueue;
sWorkParams params;
}sTaskData, *sTaskDataPtr;
long gNumProcessors;
sTaskDataPtr gTaskData;
MPQueueID gNotificationQueue;
void fStartMP( void ) {
OSErr theErr;
long i;
theErr = noErr;
/* シングルプロセッサモードを前提とする */
gNumProcessors = 1;
/* 残されているグローバルを初期化する */
gTaskData = NULL;
gNotificationQueue = NULL;
/* ライブラリが存在する場合は、タスクを作成する */
/* (シングル CPU システム上にはタスクは存在しない) */
if( MPLibraryIsLoaded() ) {
gNumProcessors = MPProcessors();
gTaskData = (sTaskDataPtr)NewPtrClear( (gNumProcessors - 1) *
sizeof( sTaskData ) );
theErr = MemError();
if( theErr == noErr )
theErr = MPCreateQueue( &gNotificationQueue );
for( i = 0; i < gNumProcessors - 1 && theErr == noErr; i++ ) {
if( theErr == noErr )
theErr = MPCreateQueue( &gTaskData[i].requestQueue );
if( theErr == noErr )
theErr = MPCreateQueue( &gTaskData[i].resultQueue );
if( theErr == noErr )
theErr = MPCreateTask( fTask, &gTaskData[i],
kMPUseDefaultStackSize, gNotificationQueue,
NULL, NULL, kMPNormalTaskOptions,
&gTaskData[i].taskID );
}
}
/* 何らかの不具合がある場合は、シングルプロセッサモードに戻る */
if( theErr != noErr ) {
fStopMP();
gNumProcessors = 1;
}
}
 |
sWorkParams 構造体は、タスクによって呼び出される関数に渡すパラメータを定義します。このブロックの内容は、実行される処理のタイプに固有のものです。その中でも、これらのパラメータは関数が処理することになっている特定のデータを定義します。
sTaskData 構造体は、アプリケーションがタスクとの間でさまざまなデータをやり取りするために使用するデータのブロックを定義します。やり取りされる
2 つの主なデータは、キュー ID と処理関数のパラメータです。
gNumProcessors グローバルには、システム内で検出されたプロセッサ数のカウントが格納されます。マルチプロセッシング
API ライブラリがロードされていない場合、または、タスクあるいはキューの作成が何らかの理由で失敗した場合、この変数の値は
1 に設定されます。アプリケーションコードの残りの部分は、gNumProcessors
が 1 の場合、アプリケーションそのものがすべての処理を実行し、何らかのマルチプロセッシング
API 呼び出しを実行しないように構成されています。
gTaskData グローバルは、動的に割り当てられた配列である sTaskData
ブロックをポイントします。作成されるそれぞれのタスクに対して 1 つのエントリが存在します。
gNotificationQueue グローバルは、終了したタスクから通知メッセージを受け取るために使用されます。この例では、すべてのタスクが一つの通知キューを共有しています。
プロセッサ数から 1 を引いた数のタスクが作成されます。それぞれのタスクは、メッセージキューの独自のペアを持ち、それによって、アプリケーションはタスクとデータをやり取りできます。キューの
ID は、タスクごとに gTaskData エントリに格納されます。この後、MPCreateTask
を使ってタスクが作成されます。先頭のパラメータは、実行タスクとなる関数へのポインタです。この例では、すべてのタスクが
fTask という同じ関数を共有しています。ある関数がマルチプロセッサによって適切に実行される場合、それは
"リエントラント" と呼ばれます。なお、"割り込みセーフ" ということは必ずしも
"リエントラント" を暗示しません。割り込みは、一般に、Mac 環境では割り込み可能ではなく、エンジニアはときどきこのことを利用します。しかし、MP
環境では、タスクコード内の任意の場所で任意のタイミングで同時に実行される複数のタスクが存在しうることを見越しておく必要があります。
第 2 のパラメータは、タスクの gTaskData エントリへのポインタです。タスクは、このブロックからリクエストおよび結果キューの
ID を抽出できるようになります。タスク当たり 2 つのキューが必要でないこともあります。たいていの場合、2
つのキューの合計値を使用することが可能です。すべてのリクエストは一つのキューにポストされ、すべての結果は別のキューに返されます。どのタスクがどのリクエストを処理するかが問題にならないときは、このように動作します。ただし、それぞれのタスクに対するパラメータは、メッセージ内に完全に含まれているか、最初のリクエストをサブミットする前にすべてのタスクに対してあらかじめ確立されている必要があります。
第 3 のパラメータは、タスクに使用する目的のスタックサイズです。それぞれのタスクは独自のスタックを持ちます。多数のタスクを作成しようとしている場合は、それぞれのタスクが受け取ることになるスタックのサイズに制限を加えることを検討してください。デフォルトのサイズは
64K であり、この値をそのまま使用すると、非常に多くのタスクを作成しようとしている場合は、マルチプロセッシング
API ライブラリが使用できるメモリ容量をかなり圧迫してしまう場合があります。スタックサイズを指定するときは、タスクの最も深い呼び出しチェーンが必要とする最小限のスペースを割り当てるように注意してください。
第 4 のパラメータは、省略可能な通知キューに対するものです。このキューは、タスクの終了シーケンスでは非常に重要になります。タスクそのものを使ってタスクの終了を厳密にコーディネートしない場合、このパラメータは事実上省略可能ではありません。警告なしにタスクを終了する場合は、通知キューは絶対に必要になります。その理由については後述します。
第 5 および 第 6 のパラメータは、タスクが終了するときに通知キューに返されます。
第 7 のパラメータは、タスク作成の性質を変更するために使用します。現在のところ、使用可能なオプションはありません。
第 8 のパラメータは、MPCreateTask によって埋められます。このパラメータは、新しく作成されたタスクの
ID となります。便宜上、このパラメータは、タスクの gTaskData エントリに格納されます。ただし、タスクがそれらの
ID を知る必要はほとんどありません。
タスクの作成中に何らかの問題が発生すると、fStopMP が呼び出されます。この呼び出しにより、それまでに作成されたすべてのものが削除されて終了します。さらに、gNumProcessors
変数の値が 1 に設定されます。その結果、アプリケーションは使用可能なプロセッサが一つしかないかのように処理を継続します。fStopMP
関数の詳細については後述します。
次に、サンプルタスクを示します。このタスクが最初に実行することは、MPCreateTask で指定されている
gTaskData エントリへのポインタを確立することです。タスクは、このブロックからリクエストおよび結果キューを取得します。その後、タスクはリクエストキュー上でリクエストを待ちます。タスクは、受け取ったメッセージを使って、呼び出す関数を選択します。関数に使用するパラメータは、タスクの
gTaskData エントリから抽出されます。このエントリは、リクエストメッセージをポストする前に、アプリケーションによってセットアップされています。アプリケーションでは、タスクがその結果メッセージを送信するまで、タスクに渡すすべてのパラメータの有効性を維持するために細心の注意を払う必要があります。たとえば、現在実行されているタスクによって書き込まれたメモリを移動したり削除したりすると、アプリケーションは破局的な事態に陥ってしまいます。
|
#define kMyRequestOne 1
#define kMyRequestTwo 2
#define kMyResultException -1
OSStatus fTask( void *parameter ) {
OSErr theErr;
sTaskDataPtr p;
Boolean finished;
long message;
theErr = noErr;
/* このタスクのユニークなデータへのポインタを取得する */
p = (sTaskDataPtr)parameter ;
/* タスクに渡した各リクエストを処理し、結果を返す */
finished = false;
while( !finished ) {
theErr = MPWaitOnQueue( p->requestQueue, (void **)&message,
NULL, NULL, kDurationForever );
if( theErr == noErr ) {
/* 呼び出す関数を選択し、パラメータを渡す */
/* パラメータは、タスクが受け取ったメッセージを送信する前に */
/* すでにセットアップされている。セレクタを使用する代わりに、 */
/* 目的の関数へのポインタを渡すこともできる */
switch( message ) {
case kMyRequestOne:
theErr = fMyTaskFunctionOne( &p->params );
break;
case kMyRequestTwo:
theErr = fMyTaskFunctionTwo( &p->params );
break;
default:
finished = true;
theErr = kMyResultException;
}
MPNotifyQueue( p->resultQueue, (void *)theErr, NULL, NULL );
}
else
finished = true;
}
/* タスクはこれで終了 */
return( theErr );
}
 |
タスクを使った処理の実行
いくつかの処理の実行を必要とするとき、アプリケーションはタスクに必要なものがすべてメモリ内に存在することを確認します。それぞれのタスクに対して、アプリケーションは、タスクに実行させたい処理のパラメータを確立し、さらに
Queue または Semaphore のいずれかを介して、その処理を実行するタスクにシグナルを送ります。タスクが実行する特定の処理は、メッセージの内部で完全に定義することができます。また、前述したように、そのタスクのために予約されているメモリブロックの中で定義することも可能です。いずれの方法も一般的に使用されます。一部のアプリケーションでは、処理を実行するためにタスクが呼び出す関数へのポインタを渡すこともあります。この方法により、一つのタスクが多数のタイプが異なる処理を実行することができます。
タスクからシグナルが出されると、アプリケーションは処理を手伝うこともできます。また、自分のイベントループに戻り、その時点で
kDurationImmediate 待機を使っているタスクをチェックする場合もあります。
kDurationImmediate が MPWaitOnQueue、MPWaitOnSemaphore、または
MPEnterCriticalRegion に指定されているとき、関数は即座に値を返します。戻り値が
kMPTimeoutErr の場合、待たれていたものは何も取得できません。つまり、メッセージは使用できず、Semaphore
はゼロで、Critical Region は別のプロセッサによって実行されたということです。
このため、アプリケーションがそのイベントループの中でタスクの結果をチェックしている場合、kDurationImmediate
待機を使って、戻り値をチェックします。戻り値が noErr の場合、結果は存在し、呼び出しによって取得されました。一方、戻り値が
kMPTimeoutErr の場合、アプリケーションが最後にチェックを行った後で、タスクは新しい結果を何も生成しませんでした。もちろん、これ以外の種類のエラーが返されることもあります。
前述したように、リクエストの処理を終了するとき、タスクは結果をポストして、処理が実行されたことをアプリケーションに知らせます。
次に、タスクを使用して処理を実行するアプリケーションの事例を示します。この場合、アプリケーションも処理の一部を実行します。なお、このアプリケーションでは、イベントは処理されません。つまり、fMyTaskFunctionOne
が処理を実行するために 1/10 秒以上の時間をとらないことが前提になっています。
|
OSErr fDoMP( long realFirstThing, long realTotalThings ) {
long i;
OSErr theErr;
long thingsPerTask;
long message;
sWorkParams appData;
theErr = noErr;
thingsPerTask = realTotalThings / gNumProcessors;
/* データ全体を構成するユニークな部分を対象に
それぞれのタスクの処理を開始する */
for( i = 0; i < gNumProcessors - 1; i++ ) {
gTaskData[i].params.firstThing = realFirstThing + thingsPerTask * i;
gTaskData[i].params.totalThings = thingsPerTask;
message = kMyRequestOne;
MPNotifyQueue( gTaskData[i].requestQueue, (void *)message,
NULL, NULL );
}
/* アプリケーションに残った処理を実行させる。gNumProcessors が */
/* 1 の場合、アプリケーションがすべての処理を実行することになり、 */
/* マルチプロセッシング API ライブラリは呼び出されない */
appData.firstThing = realFirstThing + thingsPerTask * i;
appData.totalThings = realTotalThings - thingsPerTask * i;
fMyTaskFunctionOne( &appData );
/* タスクが終了するまで待機する */
for( i = 0; i < gNumProcessors - 1; i++ )
MPWaitOnQueue( gTaskData[i].resultQueue, (void **)&message,
NULL, NULL, kDurationForever );
return( theErr );
}
 |
このようなアプローチは多数の現実のアプリケーションで使用されています。特に、巨大なデータブロックを変形するアプリケーションに最適です。データはタスクで使用するためにサイズの等しい部分に分割され、タスクが開始されます。さらに、残されたサイズが等しくない部分はアプリケーションによって処理されます。残された部分の処理が終了すると、アプリケーションはタスクが終了するのを待ちます。なお、データがプロセッサの数で割り切れるとは無条件に想定しないでください。この誤りは経験豊かなエンジニアでさえ犯す場合があります。
複数のファイルを同時にコンパイルしようとする開発環境など、サイズが等しくないサイズの大きな部分を処理するアプリケーションでは、異なるアプローチが必要になります。このようなアプリケーションではイベントループを使用し、それぞれのタスクが割り当てられた処理を終了するとき、必要に応じて、新しい処理をタスクに割り当てるようにします。
タスクの終了
アプリケーションが終了するとき、MPTerminateTask を呼び出します。ただし、MPTerminateTask
は実行中のタスクを即座に削除するわけではありません。それが将来の適切な時期に削除されるというフラグを設定するだけです。このため、タスクが実際に終了するまで、タスクが使用しているリソースを削除しないことが非常に重要です。このことを確実に行うには、MPCreateTask
呼び出しのために用意された通知キューを待ってください。MPTerminateTask
を呼び出すたびに、メッセージとしての通知キューを即座に待つようにしてください。通知キューを受け取れば、タスクがもはや実行されていないことを確認でき、さらに共有リソースを削除しても安全であることがわかります。
次の fStopMP 関数は、アプリケーションが終了しようとするときに実行されるものの事例です。fStopMP
に関して注意すべき重要ポイントの一つは、MPTerminateTask を使ってタスクが終了されたらすぐに、通知キューにメッセージが到着するまで関数の実行を一時停止することです。
|
void fStopMP( void ) {
long i;
if( gTaskData != NULL ) {
for( i = 0; i < gNumProcessors - 1; i++ ) {
if( gTaskData[i].taskID != NULL ) {
MPTerminateTask( gTaskData[i].taskID, noErr );
MPWaitOnQueue( gNotificationQueue, NULL,
NULL, NULL, kDurationForever );
}
if( gTaskData[i].requestQueue != NULL )
MPDeleteQueue( gTaskData[i].requestQueue );
if( gTaskData[i].resultQueue != NULL )
MPDeleteQueue( gTaskData[i].resultQueue );
}
if( gNotificationQueue != NULL ) {
MPDeleteQueue( gNotificationQueue );
gNotificationQueue = NULL;
}
DisposePtr( (Ptr)gTaskData );
gTaskData = NULL;
}
}
 |
マルチプロセッシングでやっていいことと、やってはいけないこと
やっていいこと
スタートアップ時に、メモリ不足のために MPLibrary をロードすることができないというメッセージが表示された場合は、ResEdit
または Resourcer を使って、“機能拡張”フォルダにある Metronub のコピーを開いてください。'sysz'
リソースの値を 2500000 に変更します。それでも問題が改善されない場合は、'sysz'
の値を一度に 1MB ずつ増加させて、問題を解決してください。
タスクはフェイスレス処理を実行する関数を呼び出します。計算中心のコードは、MP タスクの使用を検討すべき唯一のタイプのコードです。このルールは、Mac
OS 8 ではかなり緩和されましたが、計算中心のコードに MP を使用することで、スループットが飛躍的に向上するのも事実です。Mac
OS 8 では、別のタイプのコードをマルチタスク化することで応答性がかなり改善されています。
リクエストおよび結果シグナルの間にタスクによって実行される処理は "実質的"
なものです。同期メソッドのいずれか経由でシグナルの送信または受信を行うには、数百マシンサイクルを要します。タスクがリクエストされた処理を完了するまでに数サイクルしかかからない場合、アプリケーションのパフォーマンスは、マルチプロセッシングを使うことで劇的に低下します。タスクは、一つのリクエスト当たり少なくとも百万サイクルを消費する必要があります。200MHz
のプロセッサでは、これは 5 ミリ秒に相当し、この文書全体で引用されている 1/10 秒の応答時間に比べると
20 倍速いことになります。
タスクがメモリの割り当てを必要としている場合は、タスクがシグナルを出す前にメモリを割り当てるか、MPAllocate
関数を使用する必要があります。MPAllocate 関数は、アプリケーションのメモリから割り当てられたメモリブロックを返します。しかし残念なことに、MPAllocate
の処理速度は非常に低速です。MPAllocate を実行するとタスクが中断されてしまい、アプリケーションがリクエストを満足することを要求し、その後でタスクをレジュームします。MPAllocate
を多用すると、タスクのスループットが非常に低下します。メモリを確保するための最善のソリューションは、あらかじめできるだけ多くのメモリを割り当ててしまうことです。これが不可能な場合は、MPAllocate
を使って、一度にサイズの大きなメモリページを一つだけ割り当て、必要に応じて、そのページからより小さなブロックを引き出します。
やってはいけないこと
68K のコードを呼び出さないでください。セカンドプロセッサ上にはエミュレータが存在しないため、ミックスドモードスイッチを実行しようとすると、エラーが発生します。
Toolbox を呼び出さないでください。Toolbox には、現在でも大量の 68K コードが残っており、さらに悪いことにその大部分がノンリエントラント(再入不可)です。たとえば、あるタスクが
NewPtr を呼び出していて、別のタスクも NewPtr を呼び出すことを決定した場合、両方のタスクはまったく同時に同一のグローバルヒープ構造体を取り扱うことになり、まず確実にこの構造体を破壊してしまいます。Mac
OS 8 では、いくつかの Toolbox ルーチンが MP タスクから呼び出せるようになります。これらのルーチンは
Mac OS 8 のインタフェースファイルの中の FOR_SYSTEM8_PREEMPTIVE フラグによって条件化されます。
未知のコードを呼び出さないでください。サードパーティがコールバックを指定するための方法を提供する場合は、タスクからその関数を呼び出さないようにしてください。コールバックが何を実行するかはわかりません。このルールには絶対にしたがってください。コールバックがリエントラントであることを特に要求しない場合、常にそうでない可能性が存在することになります。
グローバルを避けてください。non-reentrancy の主な原因はグローバルの取り扱いにあります。グローバル、グローバルステート、あるいはグローバルによってポイントされるバッファを取り扱うタスクは、同期テクニックを使って、他のタスクが同時に同じ処理を行わないようにする必要があります。読み込み専用のグローバルなら安全です。
割り込み時に MP API ルーチンを呼び出さないでください。厳密に言うと、マルチプロセッシング
API ライブラリは、リエントラントではありません。マルチプロセッシングタスクからは、いつでも任意のマルチプロセッシング
API ルーチンを呼び出すことができますが、遅延タスク、タイムマネージャタスク、あるいはその他のシステム割り込みハンドラからこれらのルーチンを呼び出してはいけません。回避方法は存在しますが、それらは不十分な対処方法であり、一般的にはお勧めできません。詳細については、Apple
DTS または DayStar までお問い合わせください。
要約
この TECHNOTE を読み終わると、マルチプロセッシング対応アプリケーションの開発に伴う基本的なステップを理解できるはずです。要するに、マルチプロセッシング
API ライブラリが使用可能であるかどうかを確認する必要があり、プロセッサの数をカウントする必要があり、複数のタスクを同期させるための方法を作成する必要があり、さらにすべてのプロセッサが常にビジーであり続けるように十分な数のタスクを作成する必要があります。タスクが作成されると、タスクとの間でユニークな情報をやり取りでき、処理を実行する必要があるとき、タスクとアプリケーションとの間で調整を行うことが可能になります。アプリケーションを終了するとき、同期オブジェクトは削除され、タスクも終了しなければなりません。
また、タスクが実行できる処理のタイプに習熟し、同時にタスクが実行できないものも正しく認識できたはずです。
参考文献
改訂履歴
オリジナル版は、1996 年 7 月に書かれました。
|
|