優先度上限プロトコルによる優先度逆転の回避・リソース共有効率化

 並行処理での優先度逆転の回避や効率性改善の実現手段として、優先度上限プロトコルがあります。優先度上限プロトコルは、リソースをロックしたプロセスの優先度を上限値に引き上げる仕組みです。
 今回はTOPPERS/ASP3での実装を例に、簡単な解説を書きたいと思います。

優先度上限プロトコルを使用しない実装

 優先度上限プロトコルは、シングルタスクのRTOSですと動きが明快です。そのためシミュレータが提供されていてPCで簡単に動かせるRTOSTOPPERS/ASP3で動作確認します。

 今回は、基本的に前エントリ「TOPPERS/ASP3をシミュレータで動かす & タスクを実装する - 千里霧中」に追加変更を加えたコードを用います。環境構築やコンフィギュレータの操作はそのエントリを参照ください。
 まずは比較対象として、優先度上限プロトコルを用いない版を説明します。

実装

 前述エントリと同じく、TASK_HOGE、TASK_HIGH_HOGEの2つのタスクを用意します。タスク優先度は、TASK_HIGH_HOGE > TASK_HOGE です。
 そのタスクのコンフィギュレーション設定は、例えば以下のように記述します。

CRE_TSK(TASK_HOGE, { TA_NULL, 10, task_hoge, 5, STACK_SIZE, NULL });
CRE_TSK(TASK_HIGH_HOGE, { TA_NULL, 11, task_highlevel_hoge, 4, STACK_SIZE, NULL });

 次にタスクの実装です。今回は、メインタスクがTASK_HOGEをウェイクアップし、そのTASK_HOGEは途中でTASK_HIGH_HOGEをウェイクアップする動作を実装します。
 まずメインタスクに以下のウェイクアップ処理を追記します。

act_tsk(TASK_HOGE);
act_tsk(TASK_HIGH_HOGE);
for (i = 0; i < task_loop; i++);
wup_tsk(TASK_HOGE);

 TASK_HOGEの実装は以下の通りです。

void task_hoge(intptr_t exinf)
{
  while (true) {
    slp_tsk();
    syslog(LOG_INFO, "hoge");
    wup_tsk(TASK_HIGH_HOGE);    
    syslog(LOG_INFO, "hoge end");
  }
}

 TASK_HIGH_HOGEの実装は以下の通りです。

void task_highlevel_hoge(intptr_t exinf)
{
  while (true) {
    slp_tsk();
    syslog(LOG_INFO, "high hoge");
    syslog(LOG_INFO, "high hoge end");
  }
}

動作

 上記のサンプルを動かすと、以下のログ出力が得られます。

hoge
high hoge
high hoge end
hoge end

 コンフィギュレーションで指定したタスク優先度の通り、TASK_HIGH_HOGEをウェイクアップしたタイミングで、実行タスクがTASK_HOGEからTASK_HIGH_HOGEにスイッチしているのがわかります。

優先度上限プロトコルを使用する実装

 次に優先度上限プロトコルを使用した例です。今回は、TASK_HOGEがTASK_HIGH_HOGEをウェイクアップする際、優先度上限プロトコルを設定したミューテックスMTX_RESOUCEをロックするように実装します。
 まずコンフィギュレーションの記述です。ミューテックスMTX_RESOUCEの設定を追加します。MTX_RESOUCEの優先度上限は全タスク中最高の値を指定します。

CRE_TSK(TASK_HOGE, { TA_NULL, 10, task_hoge, 5, STACK_SIZE, NULL });
CRE_TSK(TASK_HIGH_HOGE, { TA_NULL, 11, task_highlevel_hoge, 4, STACK_SIZE, NULL });
CRE_MTX(MTX_RESOUCE, { TA_CEILING, 1 });

 そしてタスク実装です。最初の実装例のうち、TASK_HOGEを以下のように変更します。MTX_RESOUCEのロック制御処理を、TASK_HIGH_HOGEウェイクアップ前後に追記しています。

void task_hoge(intptr_t exinf)
{
  while (true) {
    slp_tsk();
    syslog(LOG_INFO, "hoge");
    loc_mtx(MTX_RESOUCE);
    wup_tsk(TASK_HIGH_HOGE);    
    syslog(LOG_INFO, "hoge end");
    unl_mtx(MTX_RESOUCE);
  }
}

動作

 上記のサンプルを動かすと、以下のログ出力が得られます。

hoge
hoge end
high hoge
high hoge end

 MTX_RESOUCEのロックを開放するまで、TASK_HIGH_HOGEのタスク実行が行われないことがわかります。ロック中、TASK_HOGEの優先度がTASK_HIGH_HOGEを上回っているということです。

優先度上限プロトコルの用途

 マルチタスクでの優先度設定は、一般的にタスクごとの優先度設定で行います。優先度上限プロトコルは、それに加えて、リソースごとの優先度設定を可能にする仕組みとも解釈できます。
 優先度上限プロトコルの用途には、一般的に以下があります。

  • 優先度逆転によるデッドロックの回避。メインの用途です。
  • リソース占有時間の最適化(ノンプリエンプティブならば、特定のリソース専有中での無用なコンテキストスイッチやタスク中断を抑止する。プリエンプティブなら特定のリソース専有処理を早く処理させる)
  • リソース制御のデッドラインの遵守・リアルタイム性の確保

 優先度上限プロトコルPOSIXスレッドのサポートが有名です。特にPOSIX準拠のRTOSでは活用の余地があります。

 なお「リソースごとの優先度設定を可能にする」というと聞こえは良いのですが、活用されている場は多くないと感じています。以下のような制約を持つためです。

  • タスクの優先度を動的に変えてしまうので、マルチタスクの設計をかえって複雑化させる場合があります。まず優先度上限プロトコルを使用しなくても良いようにタスク設計を工夫したほうが良いことがあります(タスクが使用するリソースを限定的に、リソースが紐づくタスクを限定的に)。
  • ラウンドロビンなどのプリエンプティブなスケジューリング(よくある典型がPOSIXを使う組み込みLinux)では、メリットをどこまで得られるかが不透明です。例えばロック時間の短縮を意図しても、具体的にどこまで時間短縮できるかが見積もりが難しいです。デッドラインの遵守などには、タスク単位の割り込み制御、CPUアフィニティの設定、FPGA・他プロセッサへの切り出し等など、別対策に頼らざるを得ない場合が多いです。

 もちろん上記の制約は状況によります。例えば重要なクリティカルセクション1、2個程度に限定して優先度上限プロトコルを適用することで、マルチタスク設計を簡略化できる場合もあります。また優先度逆転を事前のコンフィギュレーションで防止できるのが便利な状況もあります。