こんにちは、ナナです。
マルチタスク制御において欠かせないセマフォを使った「排他制御」について学びましょう。
排他制御は、プログラムを「便利にするための機能」というよりは、プログラムを「問題なく動かすための機能」のため、おろそかにしやすい傾向があります。
ただし「排他制御」を侮ってはいけません。
排他制御に対する知識不足のために、とんでもない問題が起きてしまったなんてことは珍しくありません。
それほど奥が深く、扱う上で専門性の高い知識が必要となるのです。
本記事では次の悩みを解消する内容となっています。
それでは、「バイナリセマフォ」を使った排他制御について解説しましょう!
「セマフォ」に関する基本的な概要やサービスコールの使い方を知りたい方は、まずは『ITRON セマフォの基礎【カウンティングセマフォの使い方】』を読んでおくとよいでしょう。
排他制御とは何なのか?
押忍!「排他」って漢字は「他を排除する」と書いてあるっすね。自分は誰も排除したくないっす。全員が仲間っすよ!
人間関係の場合はその心意気で過ごしてあげてね。今回はプログラムの世界における「排他」の考え方と使い方を学んでもらうよ。
時には「他を排除する」ことも大事なんですよ。
「排他」という言葉は、少しネガティブな印象を与えるかもしれません。しかし、「排他」とは守るために必要なのです。
いったい「何を守るのか?」
それは、「資源」を守るためのものなのです。
排他に対するイメージを持つ
まずは「排他」の意味をしっかりと捉えておきましょう。
皆さんは普段の生活の中で、他の人たちと共用して使うものってありますよね。
例えば寮生活をしている人は、1つしかない電子レンジをみんなの共用物として利用しているかもしれません。
つまり、電子レンジという共用資源は、2人同時には使えないということです。電子レンジを使いたければ、使い終わるのを待つしかないのです。
このようなプログラムを実現する手段が「排他制御」なのです。
本サイトにおいて「タスク」とは「人」である、と解説しています。つまり、現実世界に起こる問題は、マルチタスクのソフトウェア世界でも問題となるということなのです。
プログラムの世界における代表的な共用資源
現実世界でイメージするのであれば、電子レンジ以外にもたくさんのものが思い浮かぶのではないでしょうか。
それでは、プログラムの世界における共用資源とは何を示すのでしょうか?
代表的な「共用資源」とは
グローバル変数
です。
プログラムから行う「変数」に対する処理とは「値を読み取る」と「値を書き込む」ことの2つです。
このプログラムは、点数が100点であった場合「100:満点です」と画面表示するものです。
一見、それほど問題を感じないように思われるかもしれませんね。
しかし、この「読み取り」「書き込み」の処理は、複数のタスクから同時実行されると困るタイミングがあるのです。
マルチタスクにおける問題とは、真の意味で「タスク」を捉えていないと感じることができません。
それだけマルチタスクにおけるソフトウェア開発は難易度が上がるのです。
複数タスクからアクセスされるグローバル変数の問題
先ほどのプログラムを、マルチタスクを踏まえた観点で問題点を見てみましょう。
「書き込み」と「読み取り」の処理は、別のタスクで動くとします。
この時に意識すべきことは、マルチタスクにおけるタスク切り替えのタイミングとは、いつ発生してもおかしくない可能性を考慮する必要がある、ということです。
まずは読み取り側のタスクで、「100」の値を読み取ったとしましょう。
このタイミングで運悪くタスク切り替えが発生し、書き込み側のタスクへディスパッチしてしまいました。
次に書き込みタスクは、問答無用で変数の値を「50」へ書きこみます。
そして、先ほどの読み取り側のタスクへと再びタスク切り替えが発生しました。
なんということでしょう。画面に表示された結果は「50点」なのに「満点です」と表示されるのです。
このように、マルチタスクにおける変数への読み書きというのは、意図せぬタイミングが不用意にやってくる危険をはらんでいるのです。
排他制御をなめてはいけない
押忍っ!物申す!確かにこの説明は理解出来るっすけど、こんな奇跡的なタイミングでタスク切り替えが発生するって現実的じゃないっすよね?
不安をあおって、お金をだまし取ろうとしてないっすか?騙されないっすよ!
いやいや、騙そうとなんてしてないよ~。君がそう思う気持ちは理解できるけどね。確かにそんな都合よくタスク切り替えが起こるのかってことだよね。
でもね、「排他制御」に関する問題をなめていると、本当に痛い目にあうんだよ。
先ほどの例を見て、「こんなの起きないでしょ!」って思ったあなた、「排他制御」というのは、万が一を考えることなんです。
排他問題に対するイメージは、「十字路で車が確認せずに突っ込んでいく」そんなイメージです。
そして「この道はわりと空いていることが多く、たまにしか車が通らない」なんてイメージがぴったりです。
そのため、2つのタスクが運悪く衝突する可能性は低いのです。ただし、可能性は0ではないのです。
さぁ、皆さんが運転手ならどうしますか?
- 交差点の手前で一時停止を行い、左右から車が来てないことを確認して進む
- 左右から車なんてこないだろうと考え、確認せずにつっこむ
排他制御を行うとは「100回に1回は衝突するかもしれない」という事故を起こさないようにすることなのです。
「排他」に関する不具合は、再現性が低く対処がしづらい傾向があります。そのため、問題の発覚と対処が遅れ、非常に大きな問題に発展することがあります。
だからこそ、「排他」に関する知識はしっかりと身に付け、「問題が起こってから対処する」のではなく、「事前に設計段階で抑える」が良い手と言えるでしょう。
バイナリセマフォによる排他制御の方法
排他に関する問題点はわかったっすけど、今日のテーマは「セマフォ」っすよね。
「排他」がどんな風に「セマフォ」と関連づくのかがわかんないっす。
この排他問題を解決するための手段が「バイナリセマフォ」なんだよ。
「セマフォ」には大きく分けて2つの種類があります。排他制御を行うためにはバイナリセマフォを利用します。
カウンティングセマフォ | 管理する資源数が2以上のセマフォ |
バイナリセマフォ | 管理する資源数が1つのみのセマフォ |
バイナリセマフォの定義方法
バイナリセマフォの作成は簡単です。CRE_SEMサービスコールに対して最大資源数と初期値を「1」に設定すればよいだけです。
//------------------------------------------------
// セマフォ定義
//------------------------------------------------
CRE_SEM(SEMID_SEM1, {TA_TFIFO, 1, 1});
バイナリセマフォを使った排他制御のプログラム
それでは、「バイナリセマフォ」を具体的に使って排他制御をどのようにするのかを解説しましょう。
先ほどのプログラムを利用して、排他処理の雰囲気を感じてみてください。
書き込み側のタスク
void setPoints(void)
{
・・・
wai_sem(SEMID_SEM1);
points = 50;
sig_sem(SEMID_SEM1);
}
読み取り側のタスク
void printPoints(void)
{
wai_sem(SEMID_SEM1);
if (points == 100)
{
printf("%d:満点です", points);
}
sig_sem(SEMID_SEM1);
}
このようにバイナリセマフォに対して、「wai_sem」と「sig_sem」で排他必要な区間を挟み込むことで排他を行います。
このような、排他必要な区域を「クリティカルセクション」と呼ぶこともあるので覚えておくとよいでしょう。
バイナリセマフォを使ったプログラム練習
押忍!自分でも「セマフォ」を使ってプログラミングしてみたいっす。課題はないっすか?
何事も自分で作ることで身に付けるものだよね。じゃあ、排他制御を自分で行ってみようね。
排他処理の必要性をまだそんなに理解されていない方に向けて、もう少しわかりやすい例で排他処理の必要性を体感いただきましょう。
構造体データに対する排他制御
構造体とは、複数のデータをパッケージ化したデータのことですね。例として「座標」を表す構造体を用意してみましょう。
typedef struct
{
int x;
int y;
} S_POS;
S_POS globalPos = {127, 26}; // 沖縄の座標
座標とは「X」と「Y」で表現され、この2つが揃って1つの正しい座標を示します。
それでは次のプログラムを見てみましょう。
グローバル変数のglobalPos構造体へ、「東京」と「沖縄」の座標を連続して書き込みをしています。
// 東京座標書き込み
globalPos.x = 139; // ①
globalPos.y = 35; // ②
// 沖縄座標書き込み
globalPos.x = 127; // ③
globalPos.y = 26; // ④
この時に気を付けなければならないのは、プログラムは1行ずつ実行されるということです。
つまり、このプログラムにおいて座標というデータは、時間とともに次のように遷移していきます。
このように不正なデータ状態を経て、正しい状態へと変化していきます。マルチタスクにおいては、この不正なデータ状態を参照することを防止する必要性があります。
構造体では、全てのデータが適切な状態となって、全体が正しい状態を示すのです。
練習課題の前準備
それでは、まずは問題が発生するプログラムを準備してみましょう。
書き込みと読み取り用にタスクは次のように用意します。本例ではMAINタスクの方が優先度を高く設定しておきます。
//------------------------------------------------
// タスク定義
//------------------------------------------------
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});
MAINタスクには読み取り、TASK1には書き込みの処理を行います。それぞれのタスク役割のイメージは、次のようなものと思ってください。
//------------------------------------------------
// 概 要:MAINタスク
//------------------------------------------------
void MAIN(VP_INT exinf)
{
while(1)
{
if (globalPos.x == 139 && globalPos.y == 35)
{
Sci_putString("Tokyo\n"); // 東京
}
else if(globalPos.x == 127 && globalPos.y == 26)
{
Sci_putString("Okinawa\n"); // 沖縄
}
else
{
Sci_putString("unknown\n"); // 不明
}
dly_tsk(1000);
}
return;
}
//------------------------------------------------
// 概 要:練習用TASK1関数
//------------------------------------------------
void TASK1(VP_INT exinf)
{
while(1)
{
// 東京座標書き込み
globalPos.x = 139;
globalPos.y = 35;
// 沖縄座標書き込み
globalPos.x = 127;
globalPos.y = 26;
}
return;
}
このプログラムを実行すると、シリアル通信にて送信されたパソコン側のTeraTerm上に存在してはならない「unknown」の状態が出力されているのがわかりますね。
Tokyo
unknown
unknown
Tokyo
Okinawa
・・・
存在してはいけない座標データの「unknown」状態を避けなければなりません。排他制御を使ってこの問題を解決しますよ。
バイナリセマフォを使った排他制御の練習課題
それでは皆さん、この問題をセマフォを使って「Tokyo」「Okinawa」の2種類のみしか出力されないように変更してください。
出来た人は解答を見てみましょう!
system.cfgにバイナリセマフォを用意します。
//------------------------------------------------
// セマフォ定義
//------------------------------------------------
CRE_SEM(SEMID_SEM1, {TA_TFIFO, 1, 1});
main.cではバイナリセマフォを使った排他処理を行います。
//------------------------------------------------
// 概 要:MAINタスク
//------------------------------------------------
void MAIN(VP_INT exinf)
{
while(1)
{
wai_sem(SEMID_SEM1);
if (globalPos.x == 139 && globalPos.y == 35)
{
Sci_putString("Tokyo\n"); // 東京
}
else if(globalPos.x == 127 && globalPos.y == 26)
{
Sci_putString("Okinawa\n"); // 沖縄
}
else
{
Sci_putString("unknown\n"); // 不明
}
sig_sem(SEMID_SEM1);
dly_tsk(1000);
}
return;
}
//------------------------------------------------
// 概 要:練習用TASK1関数
//------------------------------------------------
void TASK1(VP_INT exinf)
{
while(1)
{
// 東京座標書き込み
wai_sem(SEMID_SEM1);
globalPos.x = 139;
globalPos.y = 35;
sig_sem(SEMID_SEM1);
// 沖縄座標書き込み
wai_sem(SEMID_SEM1);
globalPos.x = 127;
globalPos.y = 26;
sig_sem(SEMID_SEM1);
}
return;
}
書き込み側と読み取り側で、クリティカルセクションとなるプログラム区間を「wai_sem」と「sig_sem」で囲います。
Okinawa
Tokyo
Tokyo
Tokyo
Okinawa
Tokyo
Tokyo
正しく排他制御を行うと、「unknown」が表示されなくなるのがわかると思います。
バイナリセマフォによる排他制御の注意点
セマフォには、使う上で様々な注意点があります。これらはしっかりと把握しておかないと、よからぬ問題に巻き込まれますよ。
獲得したら必ず返却する
バイナリセマフォは排他目的で利用します。
排他のためにセマフォの「獲得」と「返却」をするわけですが、返却は必ず実施しましょう。
void subfunc(int num)
{
wai_sem(SEMID_SEM1); // 獲得
if (num < 0)
{
// 異常時は終了
return;
}
// グローバル変数の更新
gNumber = num;
sig_sem(SEMID_SEM1); // 返却
return;
}
例えばこのプログラムは異常発生時に「sig_sem」をせずにreturnしまっていますね。このようなケースは案外発生しやすいので注意が必要です。
所有権の概念がない
wai_sem/sig_semサービスコールによりセマフォの獲得/返却ができますが、セマフォには所有権という概念がありません。
そのため、とあるタスクにおいてセマフォを獲得せずに返却するといったことが可能です。
void MAIN(VP_INT exinf)
{
while(1)
{
sig_sem(SEMID_SEM1);
Sci_putString("MAIN sig sem\n");
dly_tsk(1000);
}
return;
}
void TASK1(VP_INT exinf)
{
while(1)
{
wai_sem(SEMID_SEM1);
Sci_putString("TASK1 wai sem\n");
dly_tsk(1000);
}
return;
}
MAIN sig sem
TASK1 wai sem
MAIN sig sem
TASK1 wai sem
MAIN sig sem
TASK1 wai sem
「セマフォ」を獲得していないのに返却もできる、それがセマフォの特徴です。
資源に同時アクセスする全ての箇所で排他制御をしないと意味がない
セマフォの考え方は「資源」というものをセマフォにて保護する、という考え方です。
システム全体で3カ所のクリティカルセクションがあったとして、そのうち1つが、セマフォによる排他制御をしていないと、他の2つの排他制御は効果的に作用しないということになります。
プログラムで示すと、次のように書き込み側はセマフォによる保護をしているが、読み取り側は保護していない場合、排他制御はできていないことを意味します。
書き込み側のタスク
void setPoints(void)
{
・・・
wai_sem(SEMID_SEM1);
points = 50;
sig_sem(SEMID_SEM1);
}
読み取り側のタスク
void printPoints(void)
{
if (points == 100)
{
printf("%d:満点です", points);
}
}
排他制御は漏れなく実施しないと意味がなくなるのです。誰か一人でもルールを守らないと、資源の保護はできませんよ。
クリティカルセクションはシンプルな処理
セマフォによるガードを行うということは、他のタスクがその終了を待っている可能性があります。
そのため、クリティカルセクション内のプログラムは「無駄なく・早く」が基本となります。
割込みハンドラではセマフォを獲得してはならない
割り込みハンドラでもグローバル変数にアクセスすることがよくあります。
注意しなければならないのは、割り込みハンドラ(正確には割り込みコンテキスト)ではwai_semサービスコールは呼んではならないことです。
wai_semはタスク状態を資源解放待ち状態へ遷移させます。しかし、割り込みはタスクではないため、待ち状態へ遷移することができません。
そのため、セマフォによる排他は行えません。
割り込みコンテキストにおいての、排他制御はセマフォではない別の仕組みにて行います。
その方法は、また別の記事で紹介しましょう。
セマフォによる排他制御に対するQ&A
押っ忍!排他って思っていたよりも奥が深いっすね。自分でやらなくちゃと思うと、大変な感じがするっすよ。
排他の考え方は、マルチタスクの動きとシステム全体の構成をしっかりと把握しないと難しいんですね。
何か質問があれば受け付けますよ。
Q:資源1つに対して保護するためのセマフォが1ついるの?
押忍!グローバル変数を資源とした場合、自分のプログラムにはグローバル変数が何個もあるっす。
セマフォ何個作ればいいんすか?
この質問はケースバイケースによって答えがあるため、ちょっと答えるのが難しい内容だね。
でも、グローバル変数1個に対してセマフォを1つ用意するのは過剰な設計と言えるだろうね。
セマフォをたくさん作りすぎると、排他制御が難しくなります。そして「デッドロック」が発生しやすくなります。
そのため、あまり過剰にセマフォは作りすぎないようにする方がよいです。
実践的には、「モジュール」と呼ばれるソフトウェア部品単位に対して1つ持たせるといった程度が現実的な落としどころになります。
つまり、対象モジュールが管理するグローバル変数全てを、そのモジュールが管理するセマフォ1つで保護するという考え方です。
モジュールという単位については『マイコン入門 モジュール分割手法を使ったシステム構築の考え方』を参考にしてください。
「デッドロック」に関しては次の記事で紹介しましょう。
Q:グローバル変数は必ずセマフォで保護しなくてはいけないの?
セマフォによる保護って面倒っすね・・・これって絶対やらないといけないっすか・・・
グローバル変数だからといって必ず保護が必要なわけではないんですよ。
次のような特徴をもつグローバル変数はセマフォによる排他制御は必要ありません。
- 読み取りしかされない
- 読み取りと書き込みが設計的にタイミングが競合しないことが保証できる
- 1つのタスクからしかアクセスされない
やはりポイントは複数のタスクから読み書きが競合するかどうかなのです。
Q:グローバル変数は保護するのにローカル変数は保護しないの?同じ資源でしょ?
グローバル変数はセマフォで保護するのに、なんでローカル変数は保護しないんすか?同じ変数の仲間っす。
仲間外れは良くないっすよ。
ローカル変数は絶対保護しないというわけではないんだけど、保護する必要性が薄いことは確かだね。
それはグローバル変数が静的メモリ、ローカル変数がスタックメモリに確保されることが影響しているね。
マルチタスクにおけるシステム開発は、様々なタスクがグローバル変数への読み書きを行う可能性があります。
そのため、グローバル変数はセマフォによる保護が必要となるケースが多いです。
では、ローカル変数はどうなのかというと、ローカル変数はスタックメモリ上に確保されるものであり、スタックメモリはタスクごとに別のメモリが用意されます。そのため、ローカル変数を複数のタスクから共用することは基本的にないのです。
よって、ローカル変数をセマフォで保護する必要は基本的にないのです。
スタックメモリと静的メモリの違いがわからない方は、『C言語入門 プログラム・静的・スタック・ヒープメモリを知ろう』を見ておきましょう。
ただし、ローカル変数のメモリをメールボックスなどを使って他のタスクから参照するようなケースでは、排他制御をどのように実現するかを考える必要があります。
バイナリセマフォにおける排他制御のまとめ
それでは「バイナリセマフォ」の特徴をまとめます。
- バイナリセマフォは資源数を1つとして定義したセマフォのこと
- バイナリセマフォは主に「排他制御」のために利用される
- 「排他制御」は資源を複数のタスクから同時利用しないための制御である