4. 初期化設定(スレッドモデル、排他制御、ローカル通信)

本章では、NEX を使うに当たっての初期化設定について記載します。

4.1. スレッドモデル

NEX ではゲームプログラムの仕様に応じて柔軟にスレッドモデルを選択することができます。

NEX は Scheduler::Dispatch() を定期的に実行することで、内部処理を進め、パケット送受信を行うよう設計されています。 パフォーマンスと使い勝手を考慮して、 Core::ThreadModeSafeTransportBufferの使用を推奨します。

4.1.1. スレッドモードの設定

4.1.1.1. 推奨設定

NEX のお勧めのスレッド設定、API の使い方を以下に示します。 この設定はパフォーマンスと使い勝手のバランスがとれた設定です。 送受信処理の別スレッド化により API 呼び出しのブロック時間が短くなり、かつ NEX の API をスレッドセーフに呼び出せるため、アプリケーションの開発が容易になります。 特に理由がなければこの設定を推奨します。

  • スレッド設定
  • NEX の扱い方
    • NEX の API を複数のスレッドから同時に呼び出せます。
      • 複数のスレッドから同時に呼び出す場合はスレッドセーフなメモリアロケータを使用してください。
    • アプリケーション側で Scheduler::Dispatch()、または Scheduler::DispatchAll() を、ゲームフレームの最初と最後に呼び出します。

補足

OS のパケット送信・受信処理は、I/O 待ちが原因で数 msec ブロックしてしまうため、OS APIを介して行うパケット送受信処理を別スレッドで行い、Scheduler::Dispatch() 内の長時間のブロックを緩和します。 NEX では、このスレッドのことをトランスポートバッファスレッドと呼んでいます。

トランスポートバッファスレッドは必要なメモリを確保してからスレッドを起動するので、トランスポートバッファスレッド内でメモリ操作を行うことはありません。 また、スレッドモードの設定にかかわらず、NEX とトランスポートバッファスレッドの間は排他されます。

もしより少ない CPU 負荷となることを望む場合は Core::ThreadModeUnsafeTransportBuffer の使用を検討してください。 Scheduler::Dispatch()、または Scheduler::DispatchAll() を自動的に呼び出したい場合は Core::ThreadModeInternalTransportBuffer の使用を検討してください。

4.1.1.2. スレッドモードの説明

Core::SetThreadMode() を使用して NEX ライブラリのスレッドモードを設定することができます。

  • Core::ThreadModeUndefined

    初期値です。Core::ThreadModeInternal と同じ動作となります。

  • Core::ThreadModeUnsafeTransportBuffer

    アプリケーションが定期的に Scheduler::Dispatch() を実行する必要があります。 トランスポートバッファスレッドが起動します。

    トランスポートバッファスレッドとの間以外の NEX 内の排他制御 (CriticalSection) が無効になるため、CPU 負荷が小さいです。 すべての NEX の API を同じスレッドから使用するか、アプリケーション側で適切に排他処理を行う必要があります。 もし正しく排他せずに NEX の API を並列に呼び出した場合は内部でそれが検出され、 Assert により停止します。 スレッドアンセーフな高速なメモリアロケータを利用できます。

  • Core::ThreadModeSafeTransportBuffer

    アプリケーションが定期的に Scheduler::Dispatch() を実行する必要があります。 トランスポートバッファスレッドが起動します。

    NEX 内の排他制御 (CriticalSection) が有効になり、排他の分 CPU 負荷が上がります。

    Scheduler::Dispatch() の実行と DO の操作が別スレッドになっている場合には、スレッドセーフなメモリアロケータを使用し、アプリケーション側で Scheduler::SystemLock() を取得した排他処理が必要となります。

  • Core::ThreadModeUnsafeUser

    アプリケーションが定期的に Scheduler::Dispatch() を実行する必要があります。 Scheduler::Dispatch() 内でのソケット呼び出しにより、数 msec ブロックする可能性があるため、メインスレッドとは別スレッドから Scheduler::Dispatch() を呼び出してください。

    NEX 内の排他制御 (CriticalSection) が無効になるため、CPU 負荷が小さいです。 すべての NEX の API を同じスレッドから使用するか、アプリケーション側で適切に排他処理を行う必要があります。 もし正しく排他せずに NEX の API を並列に呼び出した場合は内部でそれが検出され、 Assert により停止します。 スレッドアンセーフな高速なメモリアロケータを利用できます。

  • Core::ThreadModeInternal

    ディスパッチ処理、通信処理共にライブラリ内部のスレッドで自動的に行われます。 アプリケーションが Scheduler::Dispatch() を呼び出す必要はありません。 NEX 内の排他制御 (CriticalSection) が有効になり、排他の分 CPU 負荷が上がります。 スレッドセーフなメモリアロケータを使用する必要があります。 NEX からのコールバックは NEX の API を呼び出したスレッドもしくはライブラリ内部のスレッド (インターナルスレッド) から行われます。 インターナルスレッドとの競合を防ぐため、適宜アプリケーション側で Scheduler::SystemLock() を取得した排他処理が必要となります。 アプリケーションが NEX の API を呼び出すタイミングによってはソケット API の呼び出し中の排他制御に引っかかり、その API 呼び出しが長時間ブロックする可能性があります。 これを避けるには Core::ThreadModeInternalTransportBuffer を使用してください。

  • Core::ThreadModeInternalTransportBuffer

    ディスパッチ処理、通信処理共にライブラリ内部のスレッドで自動的に行われます。 アプリケーションが Scheduler::Dispatch() を呼び出す必要はありません。 NEX 内の排他制御 (CriticalSection) が有効になり、排他の分 CPU 負荷が上がります。 スレッドセーフなメモリアロケータを使用する必要があります。 NEX からのコールバックは NEX の API を呼び出したスレッドもしくはライブラリ内部のスレッド (インターナルスレッド) から行われます。 インターナルスレッドとの競合を防ぐため、適宜アプリケーション側で Scheduler::SystemLock() を取得した排他処理が必要となります。 トランスポートバッファスレッドが起動します。

4.1.1.3. スレッドモードの比較とスレッドの種類

Table 4.1 スレッドモードの比較
スレッドモード トランスポートバッファスレッド インターナルスレッド スレッドセーフ スレッドアンセーフなメモリアロケータ利用可
Core::ThreadModeUnsafeTransportBuffer × ×
Core::ThreadModeSafeTransportBuffer ×
Core::ThreadModeUnsafeUser × × ×
Core::ThreadModeInternal × ×
Core::ThreadModeInternalTransportBuffer ×

NEX で扱われる主なスレッドの種類は以下の通りです。

  • トランスポートバッファスレッド

    NEX 内部で生成されるスレッドです。パケットの送受信を行います。送信用と受信用の計 2 スレッドあります。 このスレッドを有効にすると NEX の API のブロック時間が低減されます。

  • インターナルスレッド

    NEX 内部で生成されるスレッドです。Scheduler::Dispatch() を定期的に呼び出し NEX の内部処理を進めます。 このスレッドを有効にするとアプリケーションから Scheduler::Dispatch() を呼び出す必要がなくなります。 Scheduler::Dispatch() を呼び出す間隔は Scheduler::SetInternalThreadDispatchInterval() で変更できます。

  • ユーザースレッド

    NEX の関数を呼び出すアプリケーションが管理するスレッドをユーザースレッドと呼びます。

NEX がアプリケーションにコールバックする際はスレッドモードによって使用されるスレッドの種類が異なります。 インターナルスレッドを使用するスレッドモードではユーザースレッドもしくはインターナルスレッドを用いてコールバックされます。 インターナルスレッドを使用しないスレッドモードではユーザースレッドを用いてコールバックされます。

スレッドセーフかどうかは NEX の API を複数のスレッドから並列に呼び出すことができるかどうかを示すと共に、CriticalSection クラスの有効/無効に影響します。 スレッドセーフではないスレッドモードでは CriticalSection::Enter() は何も行いません。

NEX はユーザースレッドとインターナルスレッドから動的にメモリをアロケートします。 つまり、インターナルスレッドを使用するスレッドモードでは、ユーザースレッドとインターナルスレッドから同時にメモリをアロケートすることがあります。 そのため、この場合は必ずスレッドセーフなメモリアロケータを MemoryManager::SetBasicMemoryFunctions() で指定する必要があります。 インターナルスレッドを使用しないスレッドモードでは複数のユーザースレッドが並列に NEX の関数を呼び出す場合にのみスレッドセーフなメモリアロケータを指定する必要があります。

4.1.1.4. スレッドの優先度

インターナルスレッドの優先度はユーザースレッド・アプリケーションのメインスレッドより高くなるように、トランスポートバッファスレッドの優先度はインターナルスレッドより高くなるように設定してください。 以下のような関係となります。

ユーザースレッド・アプリケーションのメインスレッド ≦ インターナルスレッド ≦ トランスポートバッファスレッド

もしくは

ユーザースレッド・アプリケーションのメインスレッド ≦ トランスポートバッファスレッド

この関係を満たさない場合は予期せぬパケット転送の遅延が発生したり、メモリ使用量が増加したりする場合があります。

詳細な説明

NEX の典型的な処理の流れは以下のようになります。

  1. アプリケーションが RMC などの NEX の送受信関数を呼び出す。
  2. Scheduler::Dispatch() の呼び出しにより NEX が内部処理を進めて送信すべきパケットをパケットキューへプッシュする。
  3. パケットキューからパケットを取り出し OS へ引き渡す。

この各処理はスレッドモードによって処理されるスレッドが変化します。

_images/Fig_Thread_ThreadLayer.png

Figure 4.1 各スレッドで処理される内容

1. の処理は Figure 4.1 において Interface Layer に相当し、2. の処理は Job Dispatch Layer に相当し、3. の処理は Transport Layer に相当します。 横軸はスレッドモードを表します。User は Core::ThreadModeUnsafeUser に、TransportBuffer は Core::ThreadModeUnsafeTransportBufferCore::ThreadModeSafeTransportBuffer に、 Internal は Core::ThreadModeInternal に、InternalTransportBuffer は Core::ThreadModeInternalTransportBuffer に該当します。 例えば Core::ThreadModeInternalTransportBuffer では Interface Layer はユーザースレッドで実行され、Job Dispatch Layer はインターナルスレッドで実行され、Transport Layer はトランスポートバッファスレッドで実行されます。 インターナルスレッドにおける内部処理でパケットをパケットキューにプッシュした際はトランスポートバッファスレッドを起床させ、即座にパケットの転送が行われるようにします。 しかし、もしトランスポートバッファスレッドの優先度がインターナルスレッドやユーザースレッド・アプリケーションのメインスレッドより低いと、トランスポートバッファスレッドを起床させても即座に実行に移らず、パケットの転送が待たされることがあります。

4.1.2. 排他処理

4.1.2.1. Scheduler::SystemLock() を取得した排他処理

Scheduler::Dispatch() を呼んでいるスレッド以外からDOのパラメータを取得・設定するときには、スレッドセーフなスレッドモードを選択するとともに、Scheduler::SystemLock()を取得した排他処理が必要です。

4.1.2.2. Scheduler::SystemLock() の利用例

以下に、ステーションのリストを取得する例を示します。

Code 4.1 アプリケーション側でNEXの排他制御を行うサンプル
{
    // 直接DOにアクセスするので、アプリケーション側で以下のような排他処理を行う必要がある
    ScopedCS oCS(Scheduler::GetInstance()->SystemLock());

    for (nn::nex::Station::SelectionIterator station;!station.EndReached();++station)
    {
        if (!station->IsLocal())
        {
            // 自分以外のStationに対して処理を行う
        }
    }
}

注意

Station::SelectionIterator は、フェールセーフのため、Scheduler::GetInstance()->SystemLock()を取っていない場合に 初期化されると、アサートエラーが発生します。

4.1.2.3. Scheduler::SystemLock() による排他が必要な API

Scheduler::SystemLock() による排他が必要な API を以下に示します。

4.1.2.4. Scheduler::SystemLock() による排他が不要な DO 関連の API

Scheduler::SystemLock() による排他が必要ない DO 関連の API を以下に示します。 DirectStream や NetZ、Core など DO とは関係がない API は、Scheduler::SystemLock() による排他が不要です。

4.1.2.5. コールバックからのリソースアクセスの排他処理

NEX がコールバックしたスレッドがアプリケーションのリソースにアクセスする場合は適切な排他処理を行ってください。

4.1.3. スレッドスタックについて

スレッドの生成には nn::os::Thread::TryStartUsingAutoStack() もしくは nn::os::Thread::TryStart() を使用しています。 nn::nex::Core::UseThreadAutoStack() の引数にtrueを指定した場合、nn::os::Thread::TryStartUsingAutoStack() を使用します。

nn::nex::Core::UseThreadAutoStack() の引数に false を指定した場合、もしくは nn::nex::Core::UseThreadAutoStack() を呼んでいない場合、nn::os::Thread::TryStart() を使用します。 この際 NEX は、 nn::nex::MemoryManager::SetBasicMemoryFunctions() で登録したメモリ確保関数を使ってスレッドスタックを用意します。

  • nn::os::Thread::TryStartUsingAutoStack に渡すスタックサイズ:28 [KB]
  • nn::os::Thread::TryStart に渡すスタックサイズ:28 [KB] ※メモリは約 32 [KB]( 32 [KB] - 1 byte )が確保されます。
  • トランスポートバッファスレッドモードの最大スレッド数:3

4.2. ローカル通信連携機能

ローカル通信連携機能とは、NetZ を他のネットワーク上で動作させるための機能です。 Pia ライブラリの NetZ 連携機能を使用することでローカル通信ネットワーク上で NetZ を動作させることが可能です。 これにより、アプリケーションの 「ローカル P2P 通信用プログラム」 と 「インターネット P2P 通信用プログラム」 を共通化することができます。

詳しくは任天堂のサポート窓口に問い合わせてください。