9. スレッド

SDK で定義されているスレッドクラス(nn::os::Thread)には、スレッドの開始、スレッドの合流、パラメータ取得、パラメータ変更といった基本的な関数のみが定義されています。アプリケーションで使用するスレッドは、このスレッドクラスを継承したクラスでなければなりません。

9.1. 初期化と開始

コンストラクタで構築された時点では、スレッドはまだ初期化も開始もされていません。スレッドの初期化と開始は、スタック領域の管理をアプリケーションで行う場合は Start() を、ライブラリが自動的に行う場合は StartUsingAutoStack() を呼び出すことで行われます。Try で始まるメンバ関数(TryStart()TryStartUsingAutoStack())を呼び出した場合は、リソース不足などが原因で失敗したときにエラーを返します。

スタック領域の管理をアプリケーションで行う場合、スタック領域としてメンバ関数に渡すオブジェクトには、スタックの底を uptr 型で返す GetStackBottom() というメンバ関数が定義されていなければなりません。スタック用のメモリブロックを確保する nn::os::StackMemoryBlock クラスと、static 領域や構造体の中に配置することのできる nn::os::StackBuffer クラスはその条件を満たしていますので、そのまま引数として渡すことができます。アプリケーションは、スレッドが終了するまでスタック領域が無効にならない(解放されない)ように注意しなければなりません。

9.1.1. システムコアでのアプリケーションのスレッドの実行

システムコアの CPU 時間の一部をアプリケーションに割り当て、システムコアでアプリケーションのスレッドを実行することができます。

システムコアでスレッドを実行するには、nn::os::SetApplicationCpuTimeLimit() でアプリケーションへの CPU 時間の割り当てを指定してから、coreNo に 1 を指定してスレッドを開始させます。CPU 時間を割り当てていない状態では、システムコアでスレッドを開始することができずにエラーとなります。

コード 9-1. アプリケーションへのシステムコアの CPU 時間の割り当て関数
nn::Result nn::os::SetApplicationCpuTimeLimit(s32 limitPercent);
s32        nn::os::GetApplicationCpuTimeLimit();

limitPercent には、アプリケーションに割り当てるシステムコアの CPU 時間の割合をパーセンテージで指定します。指定可能な値の範囲は 5~30 です。割り当てられる時間は 2 ミリ秒を一周期として、その周期の先頭から指定のパーセントに当たる時間です。つまり、25 を指定した場合は 0.5 ミリ秒(= 2 * 25 / 100 ミリ秒)がアプリケーションに割り当てられ、残りの 1.5 ミリ秒がシステムに割り当てられることになります。

現在割り当てられている CPU 時間の割合は nn::os::GetApplicationCpuTimeLimit() で取得することができます。初期状態では CPU 時間が割り当てられていないため、0 が返されます。

注意:

一度アプリケーションに CPU 時間を割り当てると、以降 0 に戻すことができません。

スレッドを実行していなくても、アプリケーションに割り当てられた期間はシステムの処理が行われません。そのため CPU 時間を割り当てた時点で、無線通信などのシステムコアで行われている処理の速度が低下します。

9.1.2. ManagedThread クラス

ManagedThread クラスは Thread クラスに機能を追加したユーティリティクラスです。スレッドとしての基本的な動作は、初期化と実行を行う関数が分離されていること以外は Thread クラスと同じです。

表 9-1. ManagedThread クラスで追加された機能

追加された機能

関連する関数

スタックに関する情報の取得

GetStackBufferBegin(), GetStackBufferEnd(), GetStackBufferSize(), GetStackBottom(), GetStackSize()

名前の保持

GetName(), SetName()

ID 取得の高速化

GetId(), GetCurrentId()

カレントスレッドに対応する
インスタンスの取得

GetCurrentThread()

スレッドの列挙

Enumerate()

スレッドの探索

FindById(), FindByStackAddress()

Thread クラスとリソースを共有するため、ManagedThread クラスの総数は作成可能なスレッド数の制限に含まれます。また、ManagedThread を使用するために nn::os::ManagedThread::InitializeEnvironment() を呼び出した時点で、スレッドローカルストレージを 2 つ消費します。

9.2. スレッド関数

スレッドがどのような処理を行うのかは、スレッドの初期化と開始を行うメンバ関数に渡すスレッド関数で記述します。スレッド関数は 0 ~ 1 個の引数を受け取る、返り値なし(void 型)の関数で記述してください。

なお、Thread クラスの Start 系関数には、引数を受け取らないスレッド関数が使用可能なものや、スレッド関数に渡す引数の型をテンプレートで指定することのできるオーバーロードが用意されています。渡された引数はスレッドのスタックにコピーされるため、テンプレートで指定する引数の型はコピー可能でなければならないこと、その分スレッドで使用可能なスタックが減少することに注意してください。

9.3. 終了と破棄

スレッド関数の終了時や、親クラスである nn::os::WaitObject クラスの Wait*() による待ち状態からの解放時にスレッドは終了します。Join() はスレッドの終了を無条件で待ちますので、タイムアウトを考慮するなどの細かい制御が必要な場合は親クラスのメンバ関数(nn::os::WaitObject::WaitOne() など)でスレッドの終了を待ったあとに Join() を呼び出してください。Join() の呼び出しでブロックされないようにする場合も同じです。

スレッドが生きている(終了していない)かどうかは IsAlive() で取得することができます。

不要になったスレッドを破棄するには Finalize() を呼び出してください。その際は、必ず下記の手順に従ってください。

Start() または TryStart() でスレッドを開始した場合は、スレッドを破棄する前に Join() を明示的に呼び出さなければなりません。Detach() は呼び出さないでください。

StartUsingAutoStack() または TryStartUsingAutoStack() でスレッドを開始した場合は、スレッドを破棄する前に Join() または Detach() を明示的に呼び出さなければなりません。Detach() を呼び出したあとは、そのスレッドに対して Finalize() とデストラクタ以外の呼び出しを行うことができなくなります。

9.4. スケジューリング

スレッドには優先度を設定することができ、スレッドの実行スケジュールは優先度によって決定されます。優先度は 0 ~ 31 の範囲で指定することができ、0 が一番高く、31 が一番低いことを表しています。標準的なスレッドには 16(DEFAULT_THREAD_PRIORITY)を指定します。

Yield() の呼び出しは、同じ優先度を持つ、ほかのスレッドに実行権を譲渡します。同じ優先度のスレッドが存在しなければ何も起きません。

Sleep() の呼び出しは、指定時間の間、スレッドを休止状態にします。

実行中のスレッドに割り込みによるスケジューリングが発生した場合、中断されたスレッドはスレッドキューの先頭に入れられ、なるべくスレッドが切り替わらないように制御されます。実行中のスレッドの優先度より低いまたは同じスレッドを新規に作成して実行しようとした場合、システムプロセスの動作が割り込むことでスケジューリングが直ちに行われず、スレッドが切り替わらない可能性があります。そのため、確実にスレッドを切り替える必要がある場合は、イベントを使用して待つことを推奨します。

注意:

Sleep() に短い時間を指定して連続呼び出しを行うことは、システムコア側に高い負荷をかけ、システム全体のパフォーマンスを低下させる要因になります。

9.5. パラメータの取得と変更

スレッド自身が持つパラメータとしては、スレッド ID、優先度があります。

各パラメータの取得には Get*()GetCurrent*() の 2 種類のメンバ関数があり、前者はインスタンスの、後者はカレントスレッドのパラメータを取得するメンバ関数です。パラメータの変更にも同じように、Change*()ChangeCurrent*() の 2 種類のメンバ関数があります。

カレントスレッド(メインスレッド)のオブジェクトは GetMainThread() の呼び出しで取得することができます。

スレッド ID

スレッドごとに異なる ID(bit32 型)が振られています。GetId() または GetCurrentId() による取得のみで、変更はできません。

優先度

スレッドスケジューリングの優先度(s32 型)を設定することができます。現在の優先度は、GetPriority() または GetCurrentPriority() を呼び出して取得することができます。優先度を変更するには、ChangePriority() または ChangeCurrentPriority() を呼び出してください。

9.6. スレッドローカルストレージ

uptr 型を格納することができるスレッドローカルストレージが、スレッドごとに 16 スロット用意されています。スレッドローカルストレージの使用は nn::os::ThreadLocalStorage クラスのインスタンスを生成することで予約することができますが、16 スロットを超えて予約しようとすると予約に失敗し、アプリケーションは PANIC で強制停止してしまいます。

スレッドローカルストレージへの値の設定は SetValue() で行い、値の取得は GetValue() で行います。スレッドの開始時に、スレッドローカルストレージの全スロットには 0 が設定されます。

また、スレッド終了時に呼び出されるコールバック関数を、nn::os::ThreadLocalStorage クラスの引数ありのコンストラクタで登録することができます。コールバック関数が呼ばれる際、スレッドローカルストレージの値が引数として渡されます。

9.7. 同期オブジェクト

スレッドセーフでないライブラリや共有リソースなどへのアクセスは、アプリケーションでスレッド間の同期を取って調停しなければなりません。SDK ではクリティカルセクションなどの同期オブジェクトを用意しています。

9.7.1. クリティカルセクション

クリティカルセクションは排他制御のための同期オブジェクトです。クリティカルセクションに侵入できるスレッドを一つだけに限定することで、同一リソースへの複数スレッドからのアクセスを禁止することができます。クリティカルセクションは後述するミューテックスよりもメモリ使用量は多くなりますが、ほとんどの場合高速に動作します。また、空きメモリがある限り、作成することのできる個数に上限はありません。

クリティカルセクションは nn::os::CriticalSection クラスで定義されています。インスタンスを生成して Initialize() または TryInitialize() で初期化したあと、Enter() または TryEnter() の呼び出しでクリティカルセクションに侵入してロックしようとします。クリティカルセクションがすでにロックされていると、Enter() でロックしようとした場合は呼び出したスレッドの実行がクリティカルセクションのロックが解除されるまでブロックされます。再帰ロックが可能ですので、ロックしたスレッドからのロック要求はブロックされずにネスト回数が 1 増加します。TryEnter() でロックしようとした場合はロックに成功したかどうかだけを返し、スレッドの実行はブロックされません。

クリティカルセクションのロックを解除するには、Leave() を呼び出します。ロックしたスレッドからの呼び出しでのみネスト回数が 1 減少し、ネスト回数が 0 になるまではロックが解除されません。

インスタンスを明示的に破棄する場合は Finalize() を呼び出してください。

nn::os::CriticalSection::ScopedLock クラスを利用すると、オブジェクトの生成からスコープを出るまでの間、クリティカルセクションをロックします。ロックの解除はスコープから外れたときに自動で行われます。

9.7.1.1. スレッド優先度の逆転

クリティカルセクションはスレッドの優先度継承を行いません。そのため、優先度の低いスレッド(A)がロックしているクリティカルセクションに対して高い優先度のスレッド(B)がロックを要求している場合、A よりも高くて B よりも低い優先度のスレッド(C)に B が実行を妨げられてしまいます。つまり、事実上 B の優先度が下がってしまい、B と C の優先度が逆転する状況になってしまいます。

9.7.2. ミューテックス

ミューテックスは排他制御のための同期オブジェクトです。クリティカルセクションと同じように、同一リソースへの複数スレッドからのアクセスを禁止することができます。クリティカルセクションとの違いは、スレッドの優先度継承を実装していることと、作成個数が 32 個に制限されていることです。優先度継承によって、優先度の低いスレッドがロックしているミューテックスに対して高い優先度のスレッドがロックを要求すると、ロックしているスレッドの優先度が一時的にロックを要求しているスレッドと同じ優先度に引き上げられます。

ミューテックスは nn::os::Mutex クラスで定義されています。インスタンスを生成して Initialize() または TryInitialize() で初期化したあと、Lock() または TryLock() の呼び出しでミューテックスをロックしようとします。ミューテックスがすでにロックされていると、Lock() でロックしようとした場合は呼び出したスレッドの実行がミューテックスのロックが解除されてロックに成功するまでブロックされます。再帰ロックが可能なミューテックスですので、同じスレッドからのロック要求があった場合はネスト回数が 1 増加します。TryLock() でロックしようとした場合はタイムアウトまでにロックが成功したかどうかだけを返し、スレッドの実行はブロックされません。

ミューテックスのロックを解除するには、Unlock() を呼び出しますが、Unlock() はロックしているスレッドでのみ呼び出すことができます。また、ネスト回数を 1 減少させ、ネスト回数が 0 になるまではロックが解除されません。

インスタンスを明示的に破棄する場合は Finalize() を呼び出してください。

nn::os::Mutex::ScopedLock クラスを利用すると、オブジェクトの生成からスコープを出るまでの間、ミューテックスをロックします。ロックの解除はスコープから外れたときに自動で行われます。

9.7.3. イベント

イベントはイベントの発生を通知する単純な同期オブジェクトです。イベントにはシグナル状態と非シグナル状態の 2 つの状態があり、イベントの発生は非シグナル状態からシグナル状態に遷移することで表されます。スレッドがイベントの発生を待つことで、スレッド間の同期を取ることができます。イベントの作成個数は 32 個に制限されています。

イベントは nn::os::Event クラスで定義されています。インスタンスを生成して Initialize() で初期化します。初期化の際には、イベントの種類を手動リセットイベントと自動リセットイベントから選択することができます。手動リセットイベントの場合、シグナル状態になるとその状態はクリアされるまで維持され、その間、そのイベントを待っているスレッドがすべて解放されます。自動リセットイベントの場合、シグナル状態になるとそのイベントを待っているスレッドのうちで優先順位の一番高いスレッドだけが解放され、すぐに非シグナル状態に戻ります。

Wait() の呼び出しでスレッドはイベント待ちになり、イベントがシグナル状態になるまで実行がブロックされます。タイムアウト時間を指定することができ、タイムアウトまでにイベントが発生したかどうかを確認することができます。タイムアウト時間に 0 を指定した場合はすぐに制御が戻り、ブロックされずにイベントの発生を確認することができます。

イベントをシグナル状態にするには Signal() を呼び出してください。イベントが手動リセットイベントで初期化されている場合は ClearSignal() を呼び出すまでシグナル状態がクリアされません。自動リセットイベントで初期化されている場合はイベント待ちのスレッドを 1 つだけ解放してシグナル状態は自動的にクリアされますが、イベント待ちのスレッドがない場合はシグナル状態のままとなります。

インスタンスを明示的に破棄する場合は Finalize() を呼び出してください。

9.7.3.1. ライトイベント

ライトイベントはスレッド間でフラグの通知を行う単純な同期オブジェクトで、機能的にはイベントとほとんど違いがありません。

ライトイベントは nn::os::LightEvent クラスで定義されています。nn::os::WaitObject::WaitAny() などで複数の同期オブジェクトを待つことができない点を除いては nn::os::Event クラスよりも優れていますので、なるべく nn::os::LightEvent クラスを使用してください。また、空きメモリがある限り、作成することのできる個数に上限はありません。

引数なしのコンストラクタで生成したインスタンスは Initialize() で初期化しなければなりません。初期化の際には、イベントの種類を手動リセットイベントと自動リセットイベントから選択することができます。それぞれの動作については nn::os::Event クラスと同じです。イベントの種類は引数ありのコンストラクタでも指定することができます。引数ありで生成したインスタンスは Initialize() による初期化は不要です。

Wait() でタイムアウト指定ができないことを除いて、Wait()Signal()ClearSignal()Finalize() の各メンバ関数は nn::os::Event クラスにある同名のメンバ関数と同じ動作をします。

nn::os::LightEvent クラスには、以下のメンバ関数が追加されています。

TryWait() によるフラグのチェックを行うことができます。フラグの状態を返り値で取得するだけでスレッドの実行はブロックされません。また、自動リセットイベントのインスタンスならば、フラグがセット(true = シグナル状態)されていた場合にだけフラグをクリア(false = 非シグナル状態)します。なお、TryWait() ではタイムアウト時間を指定することができます。

Pulse() はフラグがセットされるの待っているスレッドの解放を行います。自動リセットイベントならば最も優先度の高いスレッドを 1 つだけ解放し、フラグをクリアします。Signal() と異なり、待っているスレッドがなくてもフラグはクリアされます。手動リセットイベントの場合は待っているスレッドすべてを解放してからフラグをクリアします。

9.7.4. セマフォ

セマフォはカウンタを持った同期オブジェクトです。セマフォの取得要求があるとカウンタが 1 減少し、取得を要求したスレッドはカウンタが 0 より大きくなるのを待ちます。セマフォが解放されるとカウンタは 1 増加し、カウンタが 0 より大きくなるとセマフォ待ちをしているスレッドのうちで優先順位の一番高いスレッドにセマフォが渡されます。セマフォは、同時にアクセス可能なスレッドの数を制限するようなリソースの管理に利用することができます。セマフォの作成個数は 8 個に制限されています。

セマフォは nn::os::Semaphore クラスで定義されています。インスタンスを生成して Initialize() または TryInitialize() で初期化します。初期化の際には、カウンタの初期値と最大値を指定します。

セマフォの取得要求は Acquire() または TryAcquire() の呼び出しで行います。カウンタが 0 以下のときに Acquire() を呼び出した場合、セマフォを取得できるまでスレッドの実行がブロックされます。TryAcquire() の呼び出しではタイムアウト時間を指定することができ、タイムアウトまでにセマフォを取得できたかどうかを確認することができます。タイムアウト時間に 0 を指定した場合はすぐに制御が戻り、ブロックされずにセマフォの取得を試すことができます。

取得していたセマフォは Release() の呼び出しで解放することができます。カウンタの増加値に 1 以外を指定することもできますが、これは初期値を 0 で作成したインスタンスに初期値を与えるときに利用するためです。セマフォの解放時は引数なしで呼び出し、カウンタの増加値が 1 になるようにしてください。

インスタンスを明示的に破棄する場合は Finalize() を呼び出してください。

9.7.4.1. ライトセマフォ

ライトセマフォはセマフォと同じ機能を持った同期オブジェクトです。ライトセマフォは nn::os::LightSemaphore クラスで定義されています。nn::os::WaitObject::WaitAny() などで複数の同期オブジェクトを待つことができない点を除いては nn::os::Semaphore クラスよりも優れていますので、なるべく nn::os::LightSemaphore クラスを使用してください。また、空きメモリがある限り、作成することのできる個数に上限はありません。

セマフォとの違いは TryInitialize() が定義されていないことです。nn::os::LightSemaphore クラスのメンバ関数は nn::os::Semaphore クラスにある同名のメンバ関数と同じ動作をします。

9.7.5. ブロッキングキュー

ブロッキングキューはスレッド間でメッセージのやり取りを安全に行うことのできる同期オブジェクトです。キューのバッファサイズを超えて要素が挿入されたときや、要素のないキューから要素を取り出そうとしたときにスレッドの実行がブロックされます。ブロッキングキューは、スレッドが多数のスレッドからのメッセージを待つような一対多、多対多でのメッセージのやり取りに利用することができます。このブロッキングキューは NITRO/TWL や Revolution の SDK ではメッセージキューと呼ばれていた機能に相当します。

ブロッキングキューには、nn::os::BlockingQueue クラスと nn::os::SafeBlockingQueue クラスの 2 種類の定義があります。前者は内部でスレッド間の同期にクリティカルセクションを使用していますので、優先度の逆転が起こるような状況ではデッドロックが発生する可能性があります。そのような可能性がある場合はミューテックスを使用している後者のクラスを使用してください。両者のメンバ変数やメンバ関数に違いはありません。

インスタンスを生成して Initialize() または TryInitialize() で初期化します。初期化の際には、キューのバッファとして uptr 型の配列とその要素数を指定します。

キューの末尾に要素(uptr 型)を挿入するには Enqueue() または TryEnqueue() を呼び出します。キューが一杯になっているときに Enqueue() を呼び出した場合、要素を挿入できるまでスレッドの実行がブロックされます。TryEnqueue() の呼び出しでは要素を挿入できたかどうかに関係なく制御が戻るため、ブロックされずに要素の挿入を試すことができます。

キューの先頭に要素を挿入するには Jam() または TryJam() を呼び出します。キューが一杯になっているときに Jam() を呼び出した場合、要素を挿入できるまでスレッドの実行がブロックされます。TryJam() の呼び出しでは要素を挿入できたかどうかに関係なく制御が戻るため、ブロックされずに要素の挿入を試すことができます。

キューの先頭から要素を取り出すには Dequeue() または TryDequeue() を呼び出します。キューが空の状態で Dequeue() を呼び出した場合、要素を取り出せるまでスレッドの実行がブロックされます。TryDequeue() の呼び出しでは要素を取り出せたかどうかに関係なく制御が戻るため、ブロックされずに要素の取り出しを試すことができます。

GetFront() または TryGetFront() を呼び出した場合もキューの先頭の要素を取得することができますが、キューの状態を変更する(要素を取り出す)ことはありません。キューが空の状態で GetFront() を呼び出した場合、要素を取得できるまで(要素が挿入されるまで)スレッドの実行がブロックされます。TryGetFront() の呼び出しでは要素を取得できたかどうかに関係なく制御が戻るため、ブロックされずに要素の取得を試すことができます。

インスタンスを明示的に破棄する場合は Finalize() を呼び出してください。

9.7.6. ライトバリア

ライトバリアは複数のスレッドが到着するのを待つ同期オブジェクトです。初期化時に指定された数のスレッドが到着するまで早く到着したスレッドを待機させますが、このクラスを N 個のスレッドの中から M 個(M < N)の到着を待つために使用することはできません。

ライトバリアは nn::os::LightBarrier クラスで定義されています。引数なしのコンストラクタで生成したインスタンスは Initialize() で初期化しなければなりません。初期化の際には待ち合わせるスレッドの数を指定します。待ち合わせるスレッドの数は引数ありのコンストラクタでも指定することができます。引数ありで生成したインスタンスは Initialize() による初期化は不要です。空きメモリがある限り、作成することのできるライトバリアの個数に上限はありません。

Await() はほかのスレッドが到着を待ちます。この関数を呼び出したスレッドの数が待ち合わせるスレッドの数になるまで、スレッドの実行がブロックされます。

9.7.7. デッドロック

クリティカルセクション、ミューテックス、セマフォを利用して複数スレッド間の排他制御を行う際には、デッドロックに陥らないように注意しなければなりません。

例えば、リソースのアクセスに 2 つのミューテックス X と Y を必要とするスレッド A と B があると仮定します。A が X をロックし、B が Y をロックした場合、A と B は永久にブロックされたまま(デッドロック)になってしまいます。

デッドロックにならないようにする単純な方法としては、リソースを必要とするスレッドすべてが同じ順番でミューテックスのロックを要求することです。

9.8. リソースの上限

スレッドや同期オブジェクト、タイマなどは、アプリケーションで同時に生成可能なインスタンスの数が制限されています。

生成可能なインスタンスの数が制限されているクラスには、以下のクラスが該当します。

上記のクラスには共通して、リソースの使用数や上限を取得する、以下のメンバ関数が定義されています。

コード 9-2. リソースの使用数、上限を取得するメンバ関数
static s32 GetCurrentCount();
static s32 GetMaxCount();

GetCurrentCount() は、インスタンスの現在の使用数を返します。

GetMaxCount() は、インスタンスの生成可能数の上限値を返します。

アプリケーションが起動した時点から、ライブラリなどが内部的にこれらのインスタンスを生成しているため、アプリケーションで使用可能なリソースはさらに制限される可能性があります。詳しくは、「システムプログラミングガイド」の「リソース上限」や各ライブラリの説明を参照してください。