こんにちは、ナナです。
セマフォを利用した排他制御において、気を付けなければならない大きな問題があります。
それが「デッドロック」です。
この現象が何を示すのか?どのような時に発生し、どのように回避をするのかを解説していきましょう。
本記事では次の悩みを解消する内容となっています。
それでは、「デッドロック」について解説しましょう!
デッドロック現象の概要
「セマフォ」についてかなりの知識を身に付けたわよ。そろそろマスターしたと言っても過言ではないわね。
いや、ちょっと待ってね。セマフォには実は扱う上で気を付けなければならない問題がいろいろとあるんですよ。
今回は「デッドロック現象」について解説しますよ。
「デッドロック」とは何か?
「デッドロック」とは、タスクが資源返却待ち状態のまま、プログラムが停止することをいいます。
そのため、「デッドロック」は絶対に起こしてはならない問題なのです。
相手の持つ資源をお互いが求めたまま膠着状態となってしまうのがデッドロックなんです。
デッドロックの発生ケースを紹介
なんか「デッドロック」って怖い現象じゃないの。どんな時にその「デッドロック」が起こるのかを教えなさいよ!
それでは、代表的なデッドロックの発生ケースを2つ紹介しましょう!
どのようなケースでデッドロックが起こるか、具体例で説明しましょう。
ケース①:複数のセマフォを複数のタスクが交互に取得しようとした時
代表的な例として、2つのセマフォを2つのタスクが交互に取得しようとした場合、タイミングによって「デッドロック」が起きます。
まずは、タスクとセマフォの資源を定義します。
//------------------------------------------------
// タスク定義
//------------------------------------------------
CRE_TSK(TSKID_MAIN, {TA_HLNG|TA_ACT, 0, MAIN, 1, 128, NULL});
CRE_TSK(TSKID_TASK1, {TA_HLNG|TA_ACT, 0, TASK1, 2, 128, NULL});
//------------------------------------------------
// セマフォ定義
//------------------------------------------------
CRE_SEM(SEMID_SEM1, {TA_TFIFO, 1, 1});
CRE_SEM(SEMID_SEM2, {TA_TFIFO, 1, 1});
各タスクでは最初のセマフォを獲得した後に、もう一つのセマフォを獲得しに行きます。ただし、それはお互いのタスクがすでに獲得済みのセマフォなのです。
void MAIN(VP_INT exinf)
{
dly_tsk(10000); // 起動時に10秒待ち
while(1)
{
// MAINタスクでSEM1を獲得
wai_sem(SEMID_SEM1);
Sci_putString("MAIN SEM1 GET\n");
dly_tsk(1000);
wai_sem(SEMID_SEM2);
Sci_putString("MAIN SEM2 GET\n");
sig_sem(SEMID_SEM2);
sig_sem(SEMID_SEM1);
dly_tsk(1000);
}
return;
}
void TASK1(VP_INT exinf)
{
dly_tsk(10000); // 起動時に10秒待ち
while(1)
{
// TASK1タスクでSEM2を獲得
wai_sem(SEMID_SEM2);
Sci_putString("TASK1 SEM2 GET\n");
dly_tsk(1000);
wai_sem(SEMID_SEM1);
Sci_putString("TASK1 SEM1 GET\n");
sig_sem(SEMID_SEM1);
sig_sem(SEMID_SEM2);
dly_tsk(1000);
}
return;
}
結果としては次のようにそれぞれの資源を獲得した後、プリントログの出力が停止します。
MAIN SEM1 GET
TASK1 SEM2 GET
これがいわゆる「デッドロック」によるシステム停止です。
ケース②:同一タスクが同じセマフォを連続してwai_semしてしまったとき
このケースはいわゆる2タスクによる「デッドロック」ではありません。1タスクのみで起こせるデッドロックです。
タスクとセマフォを1つ用意します。
//------------------------------------------------
// タスク定義
//------------------------------------------------
CRE_TSK(TSKID_MAIN, {TA_HLNG|TA_ACT, 0, MAIN, 1, 128, NULL});
//------------------------------------------------
// セマフォ定義
//------------------------------------------------
CRE_SEM(SEMID_SEM1, {TA_TFIFO, 1, 1});
MAINタスクでは同一のセマフォを連続でwai_semします。
void MAIN(VP_INT exinf)
{
dly_tsk(10000); // 起動時に10秒待ち
while(1)
{
wai_sem(SEMID_SEM1); // 1回目
Sci_putString("MAIN SEM1 GET\n");
wai_sem(SEMID_SEM1); // 2回目
Sci_putString("MAIN SEM1 GET\n");
sig_sem(SEMID_SEM1);
sig_sem(SEMID_SEM1);
dly_tsk(1000);
}
return;
}
このプログラムを実行すると、2回目の「wai_sem」で返却待ちが無限に続き、プログラムが停止することになります。
MAIN SEM1 GET
「こんなプログラム書かないよ~」と思うかもしません。これはあくまでもわかりやすくしたケースです。
実際このデッドロック問題は、セマフォの獲得/返却が異なる関数で行われる時に比較的よく発生します。
このように、呼び出した関数先でセマフォを獲得している場合もケアする必要があり、プログラム構成をいかにうまく設計するかの技術が求められることになります。
開発規模が大きくなると、呼び出した関数の中で思わぬセマフォを獲得/返却してたりします。その時にこの罠は発動します。
デッドロックの回避策
こんな恐ろしい現象とどう立ち向かえばいいの?何かあるんでしょ!対策が!さっさと教えなさいよ!
デッドロックはもちろん設計やプログラム方法次第で回避可能です。どのような点に注意すべきかを解説しましょう。
それでは、このようなデッドロックはどのように回避すべきなのでしょうか。その方法を整理しましょう。
デッドロック発生ケース①に対する回避策
複数タスクにおけるデッドロックは次の方法で回避できます。
- 各タスクのセマフォ獲得順序を同じにする
- セマフォを1つに統合する
- セマフォの獲得は同時に1つしか行わないように設計する
デッドロック発生ケース②に対する回避策
1タスクにおけるデッドロックには次の回避方法があります。
- 関数の呼び出し元でセマフォを獲得/返却する設計とする
- 再起ミューテックスを利用するように変更する
「ミューテックス」オブジェクトに関してはもう少し先の記事で紹介しましょう。