DirectPlay コールバック関数とマルチスレッドに関する問題
DirectPlay コールバック関数とマルチスレッドに関する問題
Microsoft® DirectPlay® と DirectPlay Voice では、DirectPlay が発行するイベントを処理するため、いくつかのコールバック関数を独自に実装する必要がある。DirectPlay はマルチスレッド化されているので、同時に複数のイベントを発行することがある。このため、重複する複数のコールバックがアプリケーションに届く可能性がある。
DirectPlay には、コールバックの指示に対応するために、スレッド プールが保持されている。コールバックは、DirectPlay が保持するプール内のスレッドから呼び出される。Microsoft Windows® 2000 および Windows XP では、このスレッド プールのサイズをプロセスごとに設定できる。また、DirectPlay は、Windows 2000 および Windows XP 上で実行されるときに I/O 完了ポートを使う。I/O 完了ポートは高度な内容であり、ここでは説明しない。 詳細については、MSDN (Microsoft Developer Network ) のドキュメントや、Microsoft Win32® のマルチスレッドに関する現在入手可能なリファレンスを参照することを推奨する。
したがって、DirectPlay コールバックで正しく信頼性の高いデータ アクセスを行うには、同期をマルチスレッド化する手段を実装しなければならない。このことを、コールバックを再入可能またはスレッドセーフにすると呼ぶ。
Windows ファミリでは、現在、マルチスレッド環境でデータを同期するために、次の 3 つの方法が用意されている。
- ミューテックス オブジェクト (相互排他的な同期オブジェクト)。
- セマフォ オブジェクト (ユーザーに対して、共有ファイルまたは他のリソースが使用中であることを示すために使うフラグ変数)。
- クリティカル セクション オブジェクト (相互排他的な同期を提供するが、1 つのプロセスのスレッドのみで使う)。
クリティカル セクション オブジェクトを使った同期方法は、Microsoft DirectX® 9.0 SDK に付属する DirectPlay Voice サンプルに示されている。ミューテックス オブジェクトまたはセマフォ オブジェクトを実装する場合、Microsoft Platform Software Development Kit (SDK) や多くの参考書籍に説明が記載されている。これらの同期方法を実装する場合、何か問題が起きたときに複雑で難しいデバッグ作業が必要になるので、それぞれの分野に関する詳細な知識が必要である。
DirectPlay のスレッド モデルは、最高の効率が得られるように最適化されている。このため、受信メッセージを含む "指示" メッセージ中に、スレッド コンテキストが切り替わることはない。
詳細については、「DirectPlay と DirectPlay Voice におけるコールバック関数の実装」を参照すること。
DirectPlay のネットワーキング コールバック
DirectPlay のネットワーキング コールバック関数は、PFNDPNMESSAGEHANDLER 型である。ネットワーキング セッションの種類に応じて、IDirectPlay8Peer::Initialize、IDirectPlay8Client::Initialize、または IDirectPlay8Server::Initialize を使ってコールバック関数のアドレスを登録する。
同期に関する問題
DirectPlay コールバックでの処理中にゲーム データの整合性を保つためには、3 つのスレッド同期オブジェクトのいずれかを採用する必要がある。
ゲーム データがどのように破損するかを説明するため、コールバックで構造体にゲーム データのパケットを挿入する場合について考える。コールバックは再入可能なので、最初のコールバックが完了する前に、他のスレッドがコールバックを実行できる。この 2 番目のスレッドが、メモリ上の同じ場所にある構造体にアクセスして、データを変更しようとすることがある。この結果、1 番目のスレッドが構造体に置いたデータが、2 番目のスレッドが構造体に置いたデータで上書きされる。これはマルチスレッドのごく簡単な例であり、スレッドが正常に同期しなくなる原因は他にもいろいろあるので、注意が必要である。
DirectPlay ネットワーキング セッションでデータを同期する方法の例については、「クリティカル セクション オブジェクトを使った DirectPlay ネットワーキング コールバックの実装」を参照すること。
ワーカー スレッド
"ワーカー スレッド" を自作するのも 1 つの方法である。ワーカー スレッドは、アプリケーションで定義されるもう 1 つのマルチスレッド コールバックであり、DirectPlay コールバックと関係なくゲーム データを処理するために作成される。この目的を達成するために、最も一般的な手段として、DirectPlay のネットワーキング コールバック スレッドでは、受け取ったデータをバッファに保存する。次に、新しいスレッドが作成され、バッファリング データを処理するように知らせるメッセージがワーカー スレッド コールバックに送信される。
マルチスレッドのパフォーマンスに関する問題と非同期処理
DirectPlay コールバック内でメッセージの処理にかける時間は、慎重に検討する必要がある。DirectPlay コールバック内で大量のデータを処理し、データをロックするメカニズムを使ってスレッドの同期をとると、他のスレッドがコールバックを実行するために待機するときにブロック問題が起きる。
一方、ワーカー スレッドを実装し、ゲーム データの処理を別のコールバックに任せると、自作のスレッドと DirectPlay で作成されたスレッドの間で CPU がコンテキストを切り替えるため、オーバーヘッドが大幅に増える危険性がある。この方法を採用できるのは、ゲーム データの処理に非常に時間がかかり、ゲームのリアルタイム処理にとって欠かせないデータでない場合に限られる。たとえば、プレーヤの位置データは、ゲーム内でプレーヤをリアルタイムに配置するために欠かせないデータなので、ワーカー スレッドで処理することは推奨されない。
コールバックから DPNSUCCESS_PENDING を返し、データ バッファへのポインタを作成し、ワーカー スレッドがそのポインタを利用できるようにすることも可能である。ワーカー スレッドは、ゲーム データの処理を終了すると、使用しているトポロジに応じて、IDirectPlay8Peer、IDirectPlay8Client、または IDirectPlay8Server の ReturnBuffer メソッドを呼び出す。
API 呼び出しにまたがるロックの保持
通常、API 呼び出しにまたがって共有リソースのロックを保持することは避ける必要がある。これは、他のスレッドとのやり取りをすべて想定することが困難なためである。次のコードでは、同期的に IDirectPlay8Peer::SendTo を呼び出しているときに、送信スレッドが誤って pObj->csSomeLock クリティカル セクションを保持している。
typedef struct _MYOBJECT{
CRITICAL_SECTION csSomeLock;
DWORD dwFlags;
.
.
.
} MYOBJECT, *PMYOBJECT;
IDirectPlay8Peer *pDP8Peer;
PMYOBJECT pObj;
.
.
.
EnterCriticalSection(&pObj->csSomeLock);
pDP8Peer->SendTo(DPNID_ALL_PLAYERS_GROUP,
&dpnBuffer, 1, 0,
NULL, NULL, DPNSEND_SYNC);
LeaveCriticalSection(&pObj->csSomeLock);
DPNSEND_NOLOOPBACK フラグが使われていないため、ローカル プレーヤは、異なるスレッドにあるアプリケーションのメッセージ ハンドラの呼び出しにより、メッセージのコピーを受信する。このメッセージ ハンドラがメッセージに応答して pObj->csSomeLock を取得しようとすると、デッドロック状態が発生する。 これは、メッセージ ハンドラが値を返すまで送信スレッドは IDirectPlay8Peer::SendTo から戻れない (したがってロックを削除できない) が、メッセージ ハンドラは送信スレッドがロックを削除するまで値を返せないためである。この代わりに、フラグまたはインデックス システムを使って、API 呼び出しを実行中にロックを解放できるようにする。
typedef struct _MYOBJECT{
CRITICAL_SECTION csSomeLock;
DWORD dwFlags;
.
.
.
} MYOBJECT, *PMYOBJECT;
IDirectPlay8Peer *pDP8Peer;
PMYOBJECT pObj;
.
.
.
EnterCriticalSection(&pObj->csSomeLock);
pObj->dwFlags |= FLAGS_SENDING;
LeaveCriticalSection(&pObj->csSomeLock);
pDP8Peer->SendTo(DPNID_ALL_PLAYERS_GROUP,
&dpnBuffer, 1, 0,
NULL, NULL, DPNSEND_SYNC);
EnterCriticalSection(&pObj->csSomeLock);
pObj->dwFlags &= ~FLAGS_SENDING;
LeaveCriticalSection(&pObj->csSomeLock);