RTOSでは、重要な処理を優先して実行するためにタスクごとに優先度を設定できます。
たとえば緊急停止を行うタスクには高い優先度を設定し、ログ保存など急がない処理には低い優先度を設定するといった設計ができます。
ところがMutexなどの共有リソースが関係すると、高優先度タスクが低優先度タスクを待たなければならない状況が発生することがあります。
高優先度Task
↓
Mutexが必要
↓
低優先度Taskが保持している
↓
高優先度Taskが待たされる
さらに中優先度タスクが実行を続けると、Mutexを持っている低優先度タスクがなかなか実行できず、高優先度タスクまで長時間待たされる可能性があります。
このような問題をPriority Inversion(優先度逆転)と呼びます。
この記事では、優先度逆転とは何かという基本から、3つのタスクを使った発生メカニズム、FreeRTOSのMutexが持つPriority Inheritance(優先度継承)、Binary Semaphoreとの違い、実際の設計で注意するポイントまで順番に解説します。
優先度逆転(Priority Inversion)とは?
優先度逆転とは、高優先度のタスクが低優先度のタスクが保持するリソースを待つことで、本来優先されるはずの高優先度タスクが実行できなくなる状態です。
たとえば、次の2つのタスクを考えてみます。
Task H 優先度:高 Task L 優先度:低
通常であればTask HがReady状態になれば、Task Lより優先して実行されます。
しかしTask LがMutexを取得していたらどうでしょうか。
Task L ↓ Mutex取得 ↓ 共有リソース使用中 Task H ↓ 同じMutexが必要 ↓ Mutexを取得できない ↓ Blocked
Task Hの方が優先度は高くても、Mutexが解放されるまでは共有リソースを利用できません。
したがってTask Hは、Task Lが処理を終えてMutexを解放するのを待つ必要があります。
低優先度タスクを待つだけなら何が問題なの?
「Task LがMutexを使い終わるまで少し待てばいいだけでは?」と思うかもしれません。
ここで問題になるのが中優先度タスクです。
Task H 優先度:高 Task M 優先度:中 Task L 優先度:低
この3つのタスクがあると、優先度逆転の問題が分かりやすくなります。
3つのタスクで優先度逆転を見てみよう
まず、低優先度のTask Lが実行されているとします。
Task L ↓ Mutex取得 ↓ 共有リソースを使用
その途中で、高優先度のTask HがReady状態になりました。
Task H ↓ 実行開始 ↓ 同じMutexが必要 ↓ Task Lが保持している ↓ Blocked
Task HはMutex待ちになるため、Task Lが再び実行できるようになります。
Task LがすぐにMutexを解放すれば、大きな問題にはなりにくいでしょう。
しかし、ここでMutexを必要としない中優先度のTask MがReady状態になったとします。
Task H 高優先度 ↓ Mutex待ちなので実行できない Task M 中優先度 ↓ Ready Task L 低優先度 ↓ Mutexを保持している
Task HはBlockedなので実行できません。
Task MとTask Lを比べるとTask Mの方が優先度が高いため、Task Mが実行されます。
Task M ↓ 実行 Task L ↓ 実行できない ↓ Mutexを解放できない Task H ↓ Mutexを取得できない
つまりTask MはMutexとは無関係なのに、その実行によってTask Lが遅れ、結果として最高優先度のTask Hまで待たされてしまいます。
なぜ「優先度逆転」と呼ぶの?
本来の優先順位は次のとおりです。
Task H > Task M > Task L
ところがMutex待ちが発生すると、実質的には次のような関係になります。
Task H ↓ Task Lが終わるまで待つ しかし Task L ↓ Task Mに実行を奪われる
結果として、高優先度のTask Hが中優先度のTask Mの影響まで受けて長く待たされることになります。
このように、本来の優先度関係とは逆転したような状態になることから「優先度逆転」と呼ばれます。
時間軸で見る優先度逆転
実行の流れを時間軸で整理してみます。
時間 →
Task H(高)
実行
↓
Mutex待ち ─────────────→ 実行
Task M(中)
├──── 実行 ────┤
Task L(低)
実行
↓
Mutex取得
───────────────→ 実行 → Mutex解放
Task HがMutexを待っている間にTask Mが実行されることで、Task LがMutexを解放するタイミングが遅くなっています。
リアルタイムシステムでは大きな問題になる
優先度逆転は、単に「処理が少し遅くなる」という問題ではありません。
RTOSを使用するシステムでは、タスクの優先度を処理の重要性や応答時間の要求に基づいて設定することがあります。
たとえば、
- 緊急停止処理
- モーター制御
- 通信処理
- センサー監視
- 制御周期の維持
などでは、一定時間以内に処理を開始・完了する必要がある場合があります。
高優先度タスクが想定以上に待たされると、システムの応答時間に影響する可能性があります。
Priority Inheritance(優先度継承)とは?
優先度逆転の影響を抑えるために使われる代表的な仕組みがPriority Inheritance(優先度継承)です。
日本語では「優先度継承」と呼ばれます。
先ほどと同じ状態を考えます。
Task H 優先度:3 ↓ Mutex待ち Task L 優先度:1 ↓ Mutex保持中
高優先度のTask HがMutexを待っている間、Mutexを保持しているTask Lの優先度を一時的に引き上げます。
通常 Task H = 3 Task M = 2 Task L = 1 優先度継承 Task H = 3 Task M = 2 Task L = 3(一時的)
Task LがTask Hの優先度を一時的に継承するイメージです。
優先度継承すると何が変わる?
Task Lの優先度が一時的に引き上げられることで、中優先度のTask MによってTask Lの実行が長時間妨げられることを防ぎやすくなります。
Task H ↓ Mutex待ち Task L ↓ Task Hの優先度を継承 ↓ Task Mより優先して実行 ↓ 共有リソース処理完了 ↓ Mutex解放 Task H ↓ Mutex取得 ↓ 実行
Mutexを解放すると、Task Lの優先度は本来の値へ戻ります。
FreeRTOSのMutexには優先度継承がある
FreeRTOSのMutexには、優先度逆転の影響を抑えるための優先度継承の仕組みがあります。
Mutexは次のように作成できます。
SemaphoreHandle_t mutex;
mutex = xSemaphoreCreateMutex();
共有リソースを使用するときにMutexを取得します。
if (xSemaphoreTake(
mutex,
portMAX_DELAY
) == pdTRUE) {
// 共有リソースを使用
xSemaphoreGive(mutex);
}
高優先度タスクがこのMutexを待つ状況では、Mutexを保持している低優先度タスクに対して優先度継承が働くことがあります。
Mutexの基本については「Mutex(ミューテックス)とは?FreeRTOSの排他制御を初心者向けに解説」も参照してください。
Binary Semaphoreでは同じではない
FreeRTOSではBinary SemaphoreとMutexは似たAPIで操作しますが、目的や性質が異なります。
| Binary Semaphore | Mutex | |
|---|---|---|
| 主な目的 | イベント通知・同期 | 共有リソースの排他制御 |
| 所有者 | 基本的になし | あり |
| 優先度継承 | なし | あり |
| ISRからGive | 可能 | 通常使用しない |
そのため、共有リソースの排他制御を目的としている場合に、単にBinary Semaphoreで代用すれば同じとは限りません。
Semaphoreについては「Semaphore(セマフォ)とは?FreeRTOSのタスク同期を初心者向けに解説」も参照してください。
FreeRTOSで優先度逆転を再現してみる
優先度逆転の考え方を理解するため、3つのタスクを作ってみましょう。
ここでは概念を分かりやすくするため、
- Task L:優先度1
- Task M:優先度2
- Task H:優先度3
とします。
SemaphoreHandle_t mutex;
void lowTask(void *pvParameters)
{
while (1) {
if (xSemaphoreTake(
mutex,
portMAX_DELAY
) == pdTRUE) {
Serial.println(
"Low: Mutex acquired"
);
// Mutexを保持した状態で処理
for (volatile int i = 0;
i < 1000000;
i++) {
}
Serial.println(
"Low: Mutex release"
);
xSemaphoreGive(mutex);
}
vTaskDelay(
pdMS_TO_TICKS(1000)
);
}
}
void mediumTask(void *pvParameters)
{
while (1) {
// Mutexとは無関係な処理
for (volatile int i = 0;
i < 1000000;
i++) {
}
vTaskDelay(
pdMS_TO_TICKS(100)
);
}
}
void highTask(void *pvParameters)
{
while (1) {
if (xSemaphoreTake(
mutex,
portMAX_DELAY
) == pdTRUE) {
Serial.println(
"High: Mutex acquired"
);
xSemaphoreGive(mutex);
}
vTaskDelay(
pdMS_TO_TICKS(100)
);
}
}
実際に優先度逆転のタイミングを再現するには、各タスクの開始タイミングや処理時間を調整する必要があります。
また、この例でxSemaphoreCreateMutex()によって作成したMutexを使用するとFreeRTOSの優先度継承が働くため、単純な優先度逆転が長時間続くのを抑える方向に動作します。
優先度継承があれば何も考えなくていい?
いいえ。優先度継承は重要な仕組みですが、万能ではありません。
まず、Mutexを保持する時間そのものが長ければ、高優先度タスクはその間待つ必要があります。
低優先度Task ↓ Mutex取得 ↓ 10秒かかる処理 ↓ Mutex解放 高優先度Task ↓ そのMutexが必要 ↓ 待つ
優先度継承によって中優先度タスクからの妨害を減らせても、共有リソースの処理自体を一瞬で終わらせられるわけではありません。
Mutexを保持する時間を短くする
優先度逆転の影響を小さくする基本は、Mutexを保持する時間をできるだけ短くすることです。
Mutex取得 ↓ 共有データを更新 ↓ Mutex解放 ○ Mutex取得 ↓ 共有データ更新 ↓ 長い計算 ↓ 通信待ち ↓ vTaskDelay() ↓ Mutex解放 △
Mutexで保護する必要がない処理までMutexの内側に入れないようにします。
高優先度タスクで長時間待たない設計も考える
高優先度タスクほど、「何を待つ可能性があるか」を確認することが重要です。
特に厳しい応答時間が要求される処理では、低優先度タスクと共有するMutexへ長時間依存する設計そのものを見直した方がよい場合があります。
たとえば共有リソースを専用タスクだけが操作し、他のタスクからQueueで要求を送る方法があります。
High Task ──┐
↓
Low Task → Queue → I2C Task → I2Cバス
↑
Other Task ─┘
こうすると複数のタスクが直接I2C Mutexを取得する構造そのものを減らせる場合があります。
KUMITATE-C3で考える優先度逆転
KUMITATE-C3で、複数のタスクから同じI2Cバスを利用するシステムを考えてみましょう。
たとえば次の3つのタスクがあります。
| タスク | 優先度 | 処理 |
|---|---|---|
| Emergency Task | 高 | I2C接続機器への緊急処理 |
| Communication Task | 中 | Wi-Fiなどの通信処理 |
| Display Task | 低 | OLED表示 |
Display TaskがI2C Mutexを取得してOLEDを更新している途中で、Emergency Taskが同じI2Cバスを必要としたとします。
Display Task(低) ↓ I2C Mutex取得 ↓ OLED更新中 Emergency Task(高) ↓ I2C Mutexが必要 ↓ Blocked
この状態でCommunication Taskが実行可能になると、優先度継承がなければ低優先度のDisplay TaskよりCommunication Taskが優先され、I2C Mutexの解放がさらに遅れる可能性があります。
FreeRTOSのMutexによる優先度継承が働けば、Display Taskの優先度を一時的に引き上げ、I2C処理を完了してMutexを解放しやすくできます。
優先度逆転とデッドロックは違う
優先度逆転とデッドロックは、どちらもMutexに関連して登場するため混同しやすい問題です。
| 優先度逆転 | デッドロック | |
|---|---|---|
| 主な問題 | 高優先度タスクが低優先度タスクに待たされる | 複数タスクが互いに待ち続ける |
| 処理 | 遅れるが進む可能性がある | 永久に進めなくなる可能性がある |
| 代表的対策 | 優先度継承・保持時間短縮 | Mutex取得順序の統一など |
デッドロックについては「デッドロックとは?FreeRTOSでタスクが動かなくなる原因と対策を初心者向けに解説」も参照してください。
優先度逆転とスタベーションも違う
Starvation(スタベーション)は、あるタスクがCPUや必要なリソースをなかなか得られず、長時間処理できない状態です。
優先度逆転 高優先度Task ↓ 低優先度Taskが持つ リソースを待つ スタベーション 特定Task ↓ CPUやリソースを なかなか得られない
どちらも「タスクがなかなか実行できない」ように見えることがありますが、原因は異なります。
優先度を高くすれば解決するわけではない
問題が起きると「重要なタスクの優先度をもっと高くすればよい」と考えたくなります。
しかし、高優先度タスクが必要とするMutexを別のタスクが保持していれば、単純に優先度を上げてもMutex待ちはなくなりません。
むしろRTOSでは、タスク優先度だけでなく、
- どの共有リソースを使うのか
- どのMutexを取得するのか
- Mutexをどれだけ長く保持するのか
- どのタスクがどのタスクを待つ可能性があるのか
- 要求される応答時間はどれくらいか
まで含めて考える必要があります。
優先度逆転を防ぐための設計ポイント
- 共有リソースの排他制御には適切にMutexを使用する
- Mutexを保持する時間をできるだけ短くする
- Mutexを取得したまま不要な待ち処理をしない
- 高優先度タスクがどのMutexを待つ可能性があるか確認する
- 共有リソースを必要以上に多くのタスクから直接操作しない
- 必要に応じて専用タスク+Queueの構成を検討する
- タスク優先度を必要以上に細かく増やさない
- Binary SemaphoreとMutexを目的に合わせて使い分ける
- ISRからMutexを使用しない
- 実際の最大待ち時間を意識して設計する
Queue・Semaphore・Mutexと優先度逆転の関係
| 仕組み | 主な目的 |
|---|---|
| Queue | タスク間でデータを渡す |
| Semaphore | イベント通知・同期 |
| Mutex | 共有リソースを排他的に利用する |
| Priority Inheritance | Mutexに関連する優先度逆転の影響を抑える |
これらは別々の機能として覚えるより、RTOS上で複数のタスクを安全に協調させるための仕組みとして関連付けて理解すると分かりやすくなります。
まとめ
優先度逆転は、高優先度タスクが低優先度タスクの保持している共有リソースを必要とすることで、本来の優先度どおりに処理できなくなる問題です。
- 高優先度タスクでもMutex待ちになることがある
- 低優先度タスクがMutexを保持していると高優先度タスクが待たされる
- 中優先度タスクが実行されると低優先度タスクのMutex解放がさらに遅れる場合がある
- これによって高優先度タスクの応答が遅れる問題を優先度逆転と呼ぶ
- FreeRTOSのMutexにはPriority Inheritance(優先度継承)がある
- 優先度継承ではMutex所有タスクの優先度を一時的に引き上げる
- Mutex解放後は本来の優先度へ戻る
- Binary SemaphoreにはMutexと同じ優先度継承はない
- Mutexを保持する時間を短くすることが重要
- 優先度継承があっても設計上のすべての問題が解決するわけではない
- 優先度逆転とデッドロックは異なる問題
RTOSでは、「高優先度だから必ずすぐ実行できる」とは限りません。
高優先度タスクが共有リソースを必要とする場合、そのリソースを誰が保持しているのか、どのくらい保持するのかまで含めて応答時間を考える必要があります。
FreeRTOSのMutexが持つ優先度継承は、そのような優先度逆転の影響を抑えるための重要な仕組みです。Mutexを単なる「同時アクセス防止」として見るだけでなく、タスク優先度との関係まで理解することで、より安全なRTOS設計につながります。

